fa156d6a59
Chat UI: - Each Claude/Codex session keeps its own chat/terminal view; Settings only sets how new sessions open. - Composer + menu: upload, project files and folders (#54, also dragged from the Files/Changes tree), context files, recent uploads, skills, plugins. - Activity side panel (progress, outputs, files read, uploads, memory, skills, connectors) with a file preview; edited-file counts per turn. - Permission-mode chip (Shift+Tab steps, read back from the pane: server/lib/pane-mode.js, GET|POST /api/run/:id/permission-mode). - Stop (Esc) while the main agent works; the status is checked per agent (with session_id /api/agents ignores its status filter) and re-read every 5 s while Stop shows. - Dropped images shown in the conversation (GET /api/run/drop/:name, the requester's own drop dir), full-size viewer. - A plain-text paste over 5,000 characters is attached as pasted-text.txt, so no prompt reaches tmux/argv limits (#68). Changes panel: every /api/lanes/:id/git* route takes ?source=<lane_sources id> (sourceFolder; ENOSOURCE otherwise) and a folder picker switches the whole panel to a workspace's extra folder (#70). Server mode S3 (one Linux user per account) — experimental, verified with fake sudo plus a P6 managed-settings spike on a test account: - member tmux listing runs in the background (cached, 15 s => unknown); - transcript snapshot copy is async, chunked, temp file + rename; - Express Layer patch only on Express 4; - tmux capturePane/sendKey go to the member's own server. Docs: the multi-user design moved in as docs/plans/server-mode/README.md (the AgentHub-Server repo is gone); S3-PLAN P6 result; LANES/API/openapi.
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
/**
|
||
* @file panePaste.ts — lets another part of the Workspace page paste text into
|
||
* a workspace's terminal (a diff-line comment from the Changes panel). Every
|
||
* mounted TerminalView registers its run id; the text goes to the pane of that
|
||
* workspace focused last — clicked into, or made the Workspace's focused pane
|
||
* (a tab, Alt+1–4, the sidebar) — else any of its panes. It is a paste —
|
||
* bracketed, so `claude` takes it as one block in its composer — and never an
|
||
* Enter: the user reads it and sends it. Only a pane on SCREEN takes it (one
|
||
* hidden behind a full-width Changes panel or a replay would swallow it
|
||
* unseen); the caller says where it went, or that it went nowhere. The one
|
||
* exception is `submitToRun`: a pane's own Chat view (`PaneChat.tsx`) sends
|
||
* what the user typed there, so it pastes AND presses Enter — and its Stop
|
||
* button presses Esc through `interruptRun`.
|
||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||
*/
|
||
|
||
interface Pane {
|
||
paste: (text: string, submit?: boolean) => void;
|
||
visible: () => boolean;
|
||
key?: (data: string) => void;
|
||
}
|
||
const panes = new Map<string, Pane>();
|
||
let lastFocused: string | null = null;
|
||
|
||
/** Registers a mounted terminal (`visible`: is it on screen now; `key`: send
|
||
* raw input, as a key press); returns its unregister. */
|
||
export function registerPane(
|
||
runId: string,
|
||
paste: (text: string, submit?: boolean) => void,
|
||
visible: () => boolean = () => true,
|
||
key?: (data: string) => void
|
||
): () => void {
|
||
const entry = { paste, visible, key };
|
||
panes.set(runId, entry);
|
||
return () => {
|
||
if (panes.get(runId) === entry) panes.delete(runId);
|
||
if (lastFocused === runId) lastFocused = null;
|
||
};
|
||
}
|
||
|
||
export function notePaneFocus(runId: string): void {
|
||
lastFocused = runId;
|
||
}
|
||
|
||
/** The tmux names of workspace `laneId`'s panes: `agenthub-lane-<id>[-slot]`. */
|
||
function ofLane(runId: string, laneId: number): boolean {
|
||
const base = `agenthub-lane-${laneId}`;
|
||
return runId === base || runId.startsWith(`${base}-`);
|
||
}
|
||
|
||
/** Pastes `text` into the pane of `runId` when it is mounted and on screen;
|
||
* false otherwise (not mounted yet, or hidden). */
|
||
export function pasteToRun(runId: string, text: string): boolean {
|
||
const p = panes.get(runId);
|
||
if (!p?.visible()) return false;
|
||
p.paste(text);
|
||
return true;
|
||
}
|
||
|
||
/** Sends `text` as a message to the pane of `runId`: a paste, then Enter.
|
||
* False when the pane is not mounted or not attached. */
|
||
export function submitToRun(runId: string, text: string): boolean {
|
||
const p = panes.get(runId);
|
||
if (!p?.visible()) return false;
|
||
p.paste(text, true);
|
||
return true;
|
||
}
|
||
|
||
/** Presses Esc in the pane of `runId` — how `claude` and `codex` stop the turn
|
||
* they are running. False when the pane is not mounted or not attached. */
|
||
export function interruptRun(runId: string): boolean {
|
||
const p = panes.get(runId);
|
||
if (!p?.visible() || !p.key) return false;
|
||
p.key("\x1b");
|
||
return true;
|
||
}
|
||
|
||
/** Pastes `text` into one of the workspace's VISIBLE panes — the one focused
|
||
* last when it is — and returns its run id; null when none is on screen. */
|
||
export function pasteToLane(laneId: number, text: string): string | null {
|
||
const ok = (id: string) => ofLane(id, laneId) && !!panes.get(id)?.visible();
|
||
const id = lastFocused && ok(lastFocused) ? lastFocused : [...panes.keys()].find(ok);
|
||
if (!id) return null;
|
||
panes.get(id)!.paste(text);
|
||
return id;
|
||
}
|