feat(ui): a Claude-Desktop-style home for every workspace
/workspace?lane=<id> opens on the workspace's home (WorkspaceHome): a prompt box whose Start opens a new session in the first free pane slot, Recents (a live session attaches its pane, any other opens its transcript, nothing resumes), and Instructions / Memory / Context / Folder. Instructions live in lanes.instructions, extra folders and uploaded files in the new lane_sources table (copies under <data dir>/workspace-context/<lane id>/); spawnRun reads them from the lane's rows and hands Claude --add-dir + an appended system prompt file, Codex --add-dir + developer_instructions. Memory is read-only. Every dashboard pane carries AGENTHUB_RUN_ID; hooks forward it and the run records which session the pane holds (a Claude pane follows /clear and /resume but not a nested claude -p), so the sidebar and notifications open the right pane (&run=). Settings loses its Projects group; Worktrees now decides which worktrees Add folder suggests.
This commit is contained in:
@@ -2739,6 +2739,37 @@ export const api = {
|
||||
* @param patch Subset of lane fields to update.
|
||||
* @returns `{ lane }` — the updated {@link Lane}.
|
||||
*/
|
||||
/** GET /api/lanes/:id/sources — the workspace's instructions, extra
|
||||
* folders and context files, and whether its tool can take them. */
|
||||
sources: (id: number) => request<LaneSourcesView>(`/lanes/${id}/sources`),
|
||||
/** PUT /api/lanes/:id/instructions — appended to every new pane's system prompt. */
|
||||
setInstructions: (id: number, instructions: string | null) =>
|
||||
request<LaneSourcesView>(`/lanes/${id}/instructions`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ instructions }),
|
||||
}),
|
||||
/** POST /api/lanes/:id/sources/folder — an extra folder the agent may use. */
|
||||
addFolder: (id: number, path: string) =>
|
||||
request<LaneSourcesView>(`/lanes/${id}/sources/folder`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ path }),
|
||||
}),
|
||||
/** POST /api/lanes/:id/sources/file — upload one context file (≤ 10 MB). */
|
||||
addFile: (id: number, file: File) =>
|
||||
request<LaneSourcesView>(`/lanes/${id}/sources/file`, {
|
||||
method: "POST",
|
||||
body: file,
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"X-File-Name": encodeURIComponent(file.name),
|
||||
},
|
||||
}),
|
||||
/** DELETE /api/lanes/:id/sources/:sourceId — a folder stays on disk; an
|
||||
* uploaded file's AgentHub copy is deleted. */
|
||||
removeSource: (id: number, sourceId: number) =>
|
||||
request<LaneSourcesView>(`/lanes/${id}/sources/${sourceId}`, { method: "DELETE" }),
|
||||
/** GET /api/lanes/:id/memory — CLAUDE.md/AGENTS.md and Claude's auto memory, read-only. */
|
||||
memory: (id: number) => request<{ files: LaneMemoryFile[] }>(`/lanes/${id}/memory`),
|
||||
update: (id: number, patch: Partial<Lane>) =>
|
||||
request<{ lane: Lane }>(`/lanes/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
/**
|
||||
@@ -3879,6 +3910,38 @@ export interface RunHandle {
|
||||
tool: LaneTool;
|
||||
}
|
||||
|
||||
/** One extra folder or uploaded context file of a workspace. */
|
||||
export interface LaneSource {
|
||||
id: number;
|
||||
kind: "folder" | "file";
|
||||
path: string;
|
||||
name: string;
|
||||
size: number | null;
|
||||
added_at: string;
|
||||
/** The folder or file is gone from disk; it is skipped at spawn. */
|
||||
missing: boolean;
|
||||
}
|
||||
|
||||
/** Response shape of GET /api/lanes/:id/sources (and of every sources write). */
|
||||
export interface LaneSourcesView {
|
||||
instructions: string;
|
||||
/** The workspace's tool can take folders and instructions (claude, codex). */
|
||||
supported: boolean;
|
||||
maxFileBytes: number;
|
||||
maxInstructions: number;
|
||||
sources: LaneSource[];
|
||||
}
|
||||
|
||||
/** One read-only memory file from GET /api/lanes/:id/memory. */
|
||||
export interface LaneMemoryFile {
|
||||
label: string;
|
||||
path: string;
|
||||
size: number;
|
||||
mtime: string;
|
||||
truncated: boolean;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Response shape of GET /api/run. */
|
||||
export interface RunListResponse {
|
||||
items: RunHandle[];
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* @file projectTreePrefs — the sidebar workspace list's viewer preferences, in
|
||||
* ONE localStorage key (`agenthub.sidebar.projects`): which workspaces are
|
||||
* expanded to their sessions (by lane id), whether a repo's untracked
|
||||
* worktrees are offered, and which
|
||||
* worktree sources (`claude` = `<repo>/.claude/worktrees/*`, `agenthub` =
|
||||
* LANES_ROOT) are hidden — globally, or per project root, where a project's own
|
||||
* list (Settings → Projects) replaces the global one. A `useSyncExternalStore` reader that caches nothing
|
||||
* expanded to their sessions (by lane id), and — Settings → Worktrees —
|
||||
* whether a workspace home's Add folder suggests the repo's untracked
|
||||
* worktrees and which sources of them (`claude` = `<repo>/.claude/worktrees/*`,
|
||||
* `agenthub` = LANES_ROOT) it leaves out. A `useSyncExternalStore` reader that caches nothing
|
||||
* but the parsed blob for the current raw string, so the sidebar and Settings
|
||||
* can never disagree. Nothing here reaches the server.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -20,15 +19,12 @@ export interface ProjectTreePrefs {
|
||||
expanded: Record<string, boolean>;
|
||||
showUntracked: boolean;
|
||||
hideSources: string[];
|
||||
/** Per project root: replaces `hideSources` for that project. */
|
||||
projectHideSources: Record<string, string[]>;
|
||||
}
|
||||
|
||||
const DEFAULTS: ProjectTreePrefs = {
|
||||
expanded: {},
|
||||
showUntracked: true,
|
||||
hideSources: [],
|
||||
projectHideSources: {},
|
||||
};
|
||||
const listeners = new Set<() => void>();
|
||||
let cache: { raw: string | null; value: ProjectTreePrefs } = { raw: null, value: DEFAULTS };
|
||||
@@ -51,10 +47,6 @@ export function readProjectTreePrefs(): ProjectTreePrefs {
|
||||
expanded: parsed.expanded && typeof parsed.expanded === "object" ? parsed.expanded : {},
|
||||
showUntracked: parsed.showUntracked !== false,
|
||||
hideSources: Array.isArray(parsed.hideSources) ? parsed.hideSources : [],
|
||||
projectHideSources:
|
||||
parsed.projectHideSources && typeof parsed.projectHideSources === "object"
|
||||
? parsed.projectHideSources
|
||||
: {},
|
||||
};
|
||||
} catch {
|
||||
value = DEFAULTS;
|
||||
@@ -72,11 +64,6 @@ export function writeProjectTreePrefs(patch: Partial<ProjectTreePrefs>): void {
|
||||
listeners.forEach((l) => l());
|
||||
}
|
||||
|
||||
/** The sources hidden for one project: its own override, else the global list. */
|
||||
export function hiddenSourcesFor(prefs: ProjectTreePrefs, root: string | null): string[] {
|
||||
return (root !== null && prefs.projectHideSources[root]) || prefs.hideSources;
|
||||
}
|
||||
|
||||
function subscribe(l: () => void) {
|
||||
listeners.add(l);
|
||||
return () => listeners.delete(l);
|
||||
|
||||
Reference in New Issue
Block a user