feat: v2.6.0 — Chat UI composer (+ menu, project files, activity panel, permission mode, Stop), git on a workspace's extra folders (#70), long paste as a file (#68), S3 server mode (experimental)

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.
This commit is contained in:
2026-10-09 15:51:19 +07:00
parent ce773a3f5a
commit fa156d6a59
114 changed files with 5831 additions and 885 deletions
+17 -5
View File
@@ -9,25 +9,28 @@
* 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.
* 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); returns its
* unregister. */
/** 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
visible: () => boolean = () => true,
key?: (data: string) => void
): () => void {
const entry = { paste, visible };
const entry = { paste, visible, key };
panes.set(runId, entry);
return () => {
if (panes.get(runId) === entry) panes.delete(runId);
@@ -63,6 +66,15 @@ export function submitToRun(runId: string, text: string): boolean {
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 {