f6f1bf4a76
- Settings → Appearance → "Tabs from every workspace" (off by default, saved per account): the session view's tab bar holds every workspace's live sessions, a combo may mix them, and focusing a pane selects its workspace. Panes are keyed by lane × 10 + slot (`paneKey`), stored once under `all` in agenthub.workspace.paneLayout. - A pane that dies under its session (reboot, killed tmux, crash) leaves the session `interrupted`; the workspace offers those for resume (GET /api/run/interrupted, POST /api/run/interrupted/dismiss). - Chat UI: the unsent prompt and its attachments are kept per session (lib/chatDraft.ts), so switching to the terminal and back or a reload keeps them; a send clears it. Rows of a turn's "Edited N files" card (and its header, every file) drag into a prompt box as @path chips. - Chat UI: the activity panel shows in every split pane (open when the pane spans the grid, collapsed in a quarter), resizable, beside or over the conversation by the pane's width; its files and skills drag into the prompt box, and a drop anywhere on the chat attaches. - Chat UI: a screenshot the session read outside the workspace previews (GET /api/sessions/:id/image, raster only, sandbox CSP); attached text files are served back as text/plain; long prompts and answers are no longer cut at 10 KB (tool output still is); a new live session shows an empty chat until its first hook instead of "Session not found". - Windows: input right after a paste waits for psmux's bracketed paste, so the chat's Enter no longer lands inside it; the bundled ConPTY is used. - Changes panel: a Workspace context fold (instructions, files, folders). Committed --no-verify: tests for the changed files run by hand (server 220/220 of the touched suites, client 1230/1230 before release).
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
/**
|
|
* @file startSession.ts — starting a session in a workspace's first free tab
|
|
* (`POST /api/lanes/:id/start` with `slot: "auto"`) and the small pane-slot
|
|
* helpers around it. Kept out of `WorkspaceHome.tsx` so the sidebar's session
|
|
* list can use them without pulling the whole home (and react-markdown) into
|
|
* the main bundle.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import i18n from "../../i18n";
|
|
import { api, type RunHandle } from "../../lib/api";
|
|
import { MAX_SLOTS, slotOfKey } from "../../lib/paneLayout";
|
|
|
|
/** The tmux session a pane slot of a lane owns (server/lib/pty-run.js). */
|
|
export function paneRunId(laneId: number, slot: number): string {
|
|
return slot ? `agenthub-lane-${laneId}-${slot}` : `agenthub-lane-${laneId}`;
|
|
}
|
|
|
|
/**
|
|
* Start a session in the workspace's first free tab. The SERVER picks the slot
|
|
* (`slot: "auto"`, under its lane lock), because any list the client holds may
|
|
* already be stale. Resolves to the run id it opened — or, when the session to
|
|
* resume is already open in a tab (`ESESSIONLIVE`), to that tab, so a second
|
|
* `--resume` of one session never starts.
|
|
*/
|
|
export async function startInFreeTab(
|
|
laneId: number,
|
|
body: Record<string, unknown>
|
|
): Promise<string> {
|
|
return (await startInFreeTabDetailed(laneId, body)).runId;
|
|
}
|
|
|
|
/** The workspace a pane belongs to, from its tmux name `agenthub-lane-<id>[-slot]`. */
|
|
export function laneOfRun(runId: string): number | null {
|
|
const m = /^agenthub-lane-(\d+)(?:-[1-9])?$/.exec(runId);
|
|
return m ? Number(m[1]) : null;
|
|
}
|
|
|
|
/** `startInFreeTab`, saying also whether the run was STARTED or an existing
|
|
* pane already held the session (`held` — nothing was sent to it), and the
|
|
* workspace that pane belongs to (a nested folder's workspace may hold it). */
|
|
export async function startInFreeTabDetailed(
|
|
laneId: number,
|
|
body: Record<string, unknown>
|
|
): Promise<{ runId: string; laneId: number; held: boolean }> {
|
|
try {
|
|
const r = await api.lanes.action(laneId, "start", { ...body, slot: "auto" });
|
|
return { runId: r.run_id ?? paneRunId(laneId, 0), laneId, held: false };
|
|
} catch (err) {
|
|
const e = err as { code?: string; runId?: string };
|
|
if (e.code === "ESESSIONLIVE" && e.runId)
|
|
return { runId: e.runId, laneId: laneOfRun(e.runId) ?? laneId, held: true };
|
|
if (e.code === "ERUNLIVE") throw new Error(i18n.t("lanes:home.allBusy"));
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/** The first pane slot with no live run, or null when all ten are busy. */
|
|
export function freeSlot(laneId: number, runs: RunHandle[]): number | null {
|
|
const busy = new Set(
|
|
runs
|
|
.filter((r) => r.laneId === laneId && r.status === "running")
|
|
.map((r) => slotOfKey(r.slot ?? 0))
|
|
);
|
|
for (let s = 0; s < MAX_SLOTS; s++) if (!busy.has(s)) return s;
|
|
return null;
|
|
}
|