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:
+56
-5
@@ -91,9 +91,34 @@ function makeErr(code, message) {
|
||||
* interactive afterward (unlike `-p`, which forces print-and-exit and closes
|
||||
* stdin).
|
||||
*/
|
||||
function buildClaudeArgv({ model, permissionMode, effort, resumeSessionId, initialPrompt }) {
|
||||
/**
|
||||
* A prompt as an argv element no CLI can read as an option: one that starts
|
||||
* with "-" (`-c fix it`) would otherwise be parsed as flags — `claude -c`
|
||||
* continues the last conversation, `-p` turns the pane into print mode. A
|
||||
* leading space keeps it a positional and changes nothing the model reads.
|
||||
*/
|
||||
function promptArg(p) {
|
||||
return p.startsWith("-") ? ` ${p}` : p;
|
||||
}
|
||||
|
||||
function buildClaudeArgv({
|
||||
model,
|
||||
permissionMode,
|
||||
effort,
|
||||
resumeSessionId,
|
||||
initialPrompt,
|
||||
addDirs = [],
|
||||
systemPromptFile = null,
|
||||
}) {
|
||||
const argv = ["claude"];
|
||||
// `--add-dir` is VARIADIC (`<directories...>`): it swallows every following
|
||||
// argument up to the next option, so it goes first, where `--permission-mode`
|
||||
// always ends it — after the prompt it would eat the prompt as a directory.
|
||||
if (addDirs.length) argv.push("--add-dir", ...addDirs);
|
||||
argv.push("--permission-mode", permissionMode || "auto");
|
||||
// From a file: the instructions can be long, and the whole argv has to fit
|
||||
// in one tmux message. `=` form, so the path is never read as an option.
|
||||
if (systemPromptFile) argv.push(`--append-system-prompt-file=${systemPromptFile}`);
|
||||
// Puts "bypass permissions" INTO the pane's shift+tab cycle without turning
|
||||
// it on: `claude --help` — "Enable bypassing all permission checks as an
|
||||
// option, without it being enabled by default." Without it the cycle is only
|
||||
@@ -104,7 +129,7 @@ function buildClaudeArgv({ model, permissionMode, effort, resumeSessionId, initi
|
||||
if (model) argv.push("--model", model);
|
||||
if (effort && EFFORT_LEVELS.has(effort)) argv.push("--effort", effort);
|
||||
if (resumeSessionId) argv.push("--resume", resumeSessionId);
|
||||
if (initialPrompt) argv.push(initialPrompt);
|
||||
if (initialPrompt) argv.push(promptArg(initialPrompt));
|
||||
return argv;
|
||||
}
|
||||
|
||||
@@ -124,6 +149,9 @@ function buildCodexArgv({
|
||||
bypass,
|
||||
initialPrompt,
|
||||
routerProfile,
|
||||
addDirs = [],
|
||||
systemPrompt = null,
|
||||
systemPromptFile = null,
|
||||
}) {
|
||||
const argv = ["codex"];
|
||||
// Before the subcommand and before the prompt: `-p` layers
|
||||
@@ -141,7 +169,15 @@ function buildCodexArgv({
|
||||
if (approval) argv.push("-a", approval);
|
||||
}
|
||||
if (effort && CODEX_EFFORTS.has(effort)) argv.push("-c", `model_reasoning_effort=${effort}`);
|
||||
if (initialPrompt) argv.push(initialPrompt);
|
||||
for (const dir of addDirs) argv.push("--add-dir", dir);
|
||||
// `-c` parses its value as TOML; a JSON string literal is a valid TOML basic
|
||||
// string, so any text — quotes, newlines — arrives as one string.
|
||||
if (systemPrompt) {
|
||||
const { codexInstructions, tomlString } = require("./lane-sources");
|
||||
const text = codexInstructions(systemPrompt, systemPromptFile);
|
||||
if (text) argv.push("-c", `developer_instructions=${tomlString(text)}`);
|
||||
}
|
||||
if (initialPrompt) argv.push(promptArg(initialPrompt));
|
||||
return argv;
|
||||
}
|
||||
|
||||
@@ -159,9 +195,10 @@ function buildCatalogArgv(entry, bin, { model, resumeLast, bypass, initialPrompt
|
||||
if (resumeLast && entry.resume) argv.push(...entry.resume);
|
||||
if (bypass && entry.bypass) argv.push(...entry.bypass);
|
||||
const p = entry.prompt;
|
||||
if (initialPrompt && p.mode === "flag") argv.push(p.flag, initialPrompt);
|
||||
if (initialPrompt && p.mode === "flag") argv.push(p.flag, promptArg(initialPrompt));
|
||||
if (initialPrompt && p.mode === "argv")
|
||||
argv.push(...(p.separator ? [p.separator] : []), initialPrompt);
|
||||
// A `--` separator already ends the options; without one, guard the prompt.
|
||||
argv.push(...(p.separator ? [p.separator, initialPrompt] : [promptArg(initialPrompt)]));
|
||||
return argv;
|
||||
}
|
||||
|
||||
@@ -274,8 +311,18 @@ function spawnRun(args) {
|
||||
|
||||
const id = runIdForLane(laneId, slot);
|
||||
const startedAt = Date.now();
|
||||
// The workspace's own folders, context files and instructions — read from
|
||||
// its rows here, the one place both spawn paths pass through, never from a
|
||||
// request (server/lib/lane-sources.js).
|
||||
const { addDirs, systemPrompt, systemPromptFile } = require("./lane-sources").launchExtras(
|
||||
laneId,
|
||||
tool
|
||||
);
|
||||
const argv = buildArgv(
|
||||
{
|
||||
addDirs,
|
||||
systemPrompt,
|
||||
systemPromptFile,
|
||||
tool,
|
||||
model,
|
||||
permissionMode,
|
||||
@@ -336,6 +383,10 @@ function spawnRun(args) {
|
||||
// node`) dies there without the `node` the user's shell would have found.
|
||||
const env = {
|
||||
AGENTHUB_LANE_ID: String(laneId),
|
||||
// Which pane this is: the CLI's hooks inherit it, so the dashboard learns
|
||||
// which session each pane holds (routes/hooks.js → bindRunSession) — the
|
||||
// sidebar opens a session's own pane by it.
|
||||
AGENTHUB_RUN_ID: id,
|
||||
...(process.env.PATH ? { PATH: process.env.PATH } : {}),
|
||||
...desktopEnv(),
|
||||
// A codex pane must read the same home the dashboard reads — its rollout
|
||||
|
||||
Reference in New Issue
Block a user