@wrongstack/webui-server 0.313.1 → 0.316.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/index.js +4717 -1291
  2. package/dist/server/agent-roster-handlers.d.ts +3 -2
  3. package/dist/server/backend-services.d.ts +33 -1
  4. package/dist/server/boot.d.ts +2 -1
  5. package/dist/server/brain-handlers.d.ts +21 -4
  6. package/dist/server/chimera-routes.d.ts +88 -0
  7. package/dist/server/codebase-index-server-control.d.ts +2 -1
  8. package/dist/server/codebase-indexing.d.ts +3 -2
  9. package/dist/server/codemap-cache.d.ts +0 -5
  10. package/dist/server/codemap-handlers.d.ts +2 -1
  11. package/dist/server/collab/controller.d.ts +0 -2
  12. package/dist/server/collab/session-registry.d.ts +2 -1
  13. package/dist/server/connection-handler.d.ts +2 -1
  14. package/dist/server/connections/auto-healer.d.ts +5 -6
  15. package/dist/server/connections/collector.d.ts +0 -3
  16. package/dist/server/connections/helpers.d.ts +0 -2
  17. package/dist/server/connections/service-actions.d.ts +0 -4
  18. package/dist/server/content-routes.d.ts +6 -1
  19. package/dist/server/conversation-operations.d.ts +18 -2
  20. package/dist/server/deadcode-handlers.d.ts +2 -1
  21. package/dist/server/design-handlers.d.ts +6 -0
  22. package/dist/server/discover-mailbox-bridge.d.ts +2 -1
  23. package/dist/server/embedded-host-adapters.d.ts +120 -2
  24. package/dist/server/embedded-message-router.d.ts +15 -1
  25. package/dist/server/entry.js +4301 -1075
  26. package/dist/server/file-handlers.d.ts +5 -2
  27. package/dist/server/git-handlers.d.ts +19 -0
  28. package/dist/server/governance-runtime.d.ts +1 -1
  29. package/dist/server/handlers.js +6 -9
  30. package/dist/server/http-server/analytics-handler.d.ts +0 -2
  31. package/dist/server/http-server/security-helpers.d.ts +0 -6
  32. package/dist/server/http-server/vector-memory-handlers.d.ts +2 -56
  33. package/dist/server/index.d.ts +12 -5
  34. package/dist/server/introspection-routes.d.ts +16 -0
  35. package/dist/server/kanban-contract-routes.d.ts +19 -1
  36. package/dist/server/kanban-decomposition-routes.d.ts +4 -1
  37. package/dist/server/kanban-dispatch.d.ts +2 -0
  38. package/dist/server/kanban-route-helpers.d.ts +21 -0
  39. package/dist/server/kanban-routes.d.ts +13 -1
  40. package/dist/server/kanban-supervisor.d.ts +2 -0
  41. package/dist/server/kanban-task-routes.d.ts +4 -1
  42. package/dist/server/lifecycle.d.ts +2 -1
  43. package/dist/server/mcp-handlers.d.ts +2 -1
  44. package/dist/server/message-dispatcher.d.ts +19 -5
  45. package/dist/server/mode-handlers.d.ts +6 -4
  46. package/dist/server/mode-operations.d.ts +15 -4
  47. package/dist/server/model-catalog-search.d.ts +2 -1
  48. package/dist/server/model-operations.d.ts +6 -2
  49. package/dist/server/pending-confirms.d.ts +27 -1
  50. package/dist/server/port-utils.d.ts +16 -2
  51. package/dist/server/pref-helpers.d.ts +1 -1
  52. package/dist/server/prefs-handlers.d.ts +43 -6
  53. package/dist/server/prefs-routes.d.ts +11 -2
  54. package/dist/server/process-handlers.d.ts +12 -5
  55. package/dist/server/project-handlers.d.ts +12 -0
  56. package/dist/server/project-watcher.d.ts +19 -10
  57. package/dist/server/provider/mutations.d.ts +0 -1
  58. package/dist/server/provider-fanout.d.ts +46 -0
  59. package/dist/server/provider-handlers.d.ts +6 -2
  60. package/dist/server/provider-routes.d.ts +3 -0
  61. package/dist/server/provider-store.d.ts +2 -1
  62. package/dist/server/requirement-intake-handlers.d.ts +0 -2
  63. package/dist/server/route-family-dispatcher.d.ts +2 -0
  64. package/dist/server/routes.d.ts +41 -5
  65. package/dist/server/sdd-board-ws-handler.d.ts +2 -2
  66. package/dist/server/server-runtime.d.ts +23 -6
  67. package/dist/server/session-agent-registry.d.ts +145 -0
  68. package/dist/server/session-agent-sessions.d.ts +49 -0
  69. package/dist/server/session-cleanup-scheduler.d.ts +3 -2
  70. package/dist/server/session-deletion.d.ts +1 -0
  71. package/dist/server/session-handlers.d.ts +82 -1
  72. package/dist/server/session-history.d.ts +4 -3
  73. package/dist/server/session-routes.d.ts +2 -0
  74. package/dist/server/session-scoped-prefs.d.ts +28 -0
  75. package/dist/server/setup-events-core-watchers.d.ts +7 -2
  76. package/dist/server/setup-events-fleet-broadcaster.d.ts +7 -2
  77. package/dist/server/setup-events-pattern-handlers.d.ts +5 -1
  78. package/dist/server/setup-events-provider-handlers.d.ts +1 -1
  79. package/dist/server/setup-events-session-helpers.d.ts +14 -1
  80. package/dist/server/setup-events-status-watcher.d.ts +7 -2
  81. package/dist/server/setup-events-subagent-handlers.d.ts +5 -1
  82. package/dist/server/setup-events-tool-handlers.d.ts +14 -1
  83. package/dist/server/setup-events.d.ts +21 -1
  84. package/dist/server/setup-screen.d.ts +3 -2
  85. package/dist/server/shell-git-routes.d.ts +4 -0
  86. package/dist/server/standalone-session-identity.d.ts +3 -3
  87. package/dist/server/start-webui-proxy-apply.d.ts +2 -1
  88. package/dist/server/start-webui-shutdown.d.ts +15 -0
  89. package/dist/server/system-prompt-handlers.d.ts +67 -0
  90. package/dist/server/system-prompt-rebuild.d.ts +45 -0
  91. package/dist/server/terminal-dashboard.d.ts +145 -0
  92. package/dist/server/types.d.ts +33 -4
  93. package/dist/server/webui-status-logger.d.ts +61 -0
  94. package/dist/server/worklist-routes.d.ts +8 -2
  95. package/dist/server/worklist-session-context.d.ts +44 -0
  96. package/dist/server/worktree-ws-handler.d.ts +2 -1
  97. package/dist/server/ws-payload-validation.d.ts +27 -2
  98. package/dist/server/ws-utils.d.ts +29 -4
  99. package/package.json +16 -15
@@ -1,13 +1,13 @@
1
- import type { EventBus } from '@wrongstack/core/kernel';
2
1
  import { AgentStatusTracker } from '@wrongstack/core/coordination';
2
+ import type { EventBus } from '@wrongstack/core/kernel';
3
3
  import { getSessionRegistry } from '@wrongstack/core/storage';
4
4
  import type { Config, Logger } from '@wrongstack/core/types';
5
- export interface StandaloneSessionIdentityPaths {
5
+ interface StandaloneSessionIdentityPaths {
6
6
  globalRoot: string;
7
7
  projectRoot: string;
8
8
  projectSlug: string;
9
9
  }
10
- export interface StandaloneSessionIdentityOptions {
10
+ interface StandaloneSessionIdentityOptions {
11
11
  config: Config;
12
12
  events: EventBus;
13
13
  logger: Logger;
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { Provider, ProviderConfig } from '@wrongstack/core/types';
24
24
  import type { WebuiDeps, WebuiMutableState } from './routes.js';
25
- export interface WebuiProxyApplyOptions {
25
+ interface WebuiProxyApplyOptions {
26
26
  state: WebuiMutableState;
27
27
  deps: WebuiDeps;
28
28
  /** Mirrors `cb.updateAutoCompactionMaxContext` in start-webui.ts. */
@@ -35,4 +35,5 @@ export interface WebuiProxyApplyOptions {
35
35
  * semantics (effective-URL comparison, serialized rebuilds).
36
36
  */
37
37
  export declare function setupWebuiProxyInstantApply(options: WebuiProxyApplyOptions): () => void;
38
+ export {};
38
39
  //# sourceMappingURL=start-webui-proxy-apply.d.ts.map
@@ -17,6 +17,9 @@ export declare function setupWebuiShutdown(options: {
17
17
  detach: () => Promise<void>;
18
18
  };
19
19
  stopHeapWatchdog: () => Promise<void>;
20
+ stopLiveStatusLogger?: (() => void) | undefined;
21
+ /** Erase the fixed status panel and restore the raw console. */
22
+ stopTerminalDashboard?: (() => void) | undefined;
20
23
  getCredentialWatcherClose: () => (() => void) | undefined;
21
24
  getProxyInstantApplyDispose: () => () => void;
22
25
  disposeRealtimeHandlers: () => void;
@@ -44,6 +47,18 @@ export declare function setupWebuiShutdown(options: {
44
47
  vectorMemoryStore: {
45
48
  close(): void;
46
49
  } | undefined;
50
+ /**
51
+ * Drain every open tab's journal synchronously. Registered as a fatal-exit
52
+ * salvage hook: this host had none at all, so a crash truncated the buffered
53
+ * tail of the leader's journal AND of every other open tab.
54
+ */
55
+ flushSessionJournalsSync?: (() => void) | undefined;
56
+ /**
57
+ * End and close the journals of every tab that is not the leader's. Without
58
+ * it a clean quit left them with no trailing `session_end`, which is exactly
59
+ * what a crash looks like — the next launch offered them all for recovery.
60
+ */
61
+ closeSessionJournals?: (() => Promise<void>) | undefined;
47
62
  globalConfigPath: string;
48
63
  }): () => void;
49
64
  //# sourceMappingURL=start-webui-shutdown.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The identity-prompt picker surface: which variants exist, what each costs,
3
+ * which one this session is running, and whether the user has ever chosen.
4
+ *
5
+ * `chosen` is the non-obvious field. The config loader materializes
6
+ * `systemPrompt: { variant: 'default' }` for every config, so the live Config
7
+ * cannot tell "the user picked Standard" from "nobody has ever asked". Only the
8
+ * raw profile config file can — the key is written explicitly on selection — so
9
+ * `chosen: false` is what lets the browser open the picker once on a fresh
10
+ * install and never nag again. It is the same signal the CLI startup menu uses
11
+ * to decide between the full menu and the one-line confirmation.
12
+ */
13
+ import { type SystemInstructionVariant, type SystemPromptVariantPaths } from '@wrongstack/core/agent';
14
+ /** Everything the picker needs from the host, injected so both WS hosts share it. */
15
+ export interface SystemPromptSurface {
16
+ /** Instruction dirs, resolved the way the SystemPromptBuilder resolves them. */
17
+ paths: () => SystemPromptVariantPaths;
18
+ /** Active profile config — the file whose `systemPrompt.variant` marks a choice. */
19
+ profileConfigPath: string;
20
+ /** The variant the running session is currently built from. */
21
+ current: () => SystemInstructionVariant;
22
+ /**
23
+ * Recompose the live system prompt for the new variant. Persistence is the
24
+ * prefs layer's job; this only refreshes the in-memory prompt so the change
25
+ * takes effect on the next turn instead of the next boot.
26
+ */
27
+ /**
28
+ * Rebuild the live identity prompt. `sessionId` names the tab that asked —
29
+ * each WebUI tab runs its own context, so the rebuild has to land on that
30
+ * one rather than on whichever session the runtime is currently pointing at.
31
+ */
32
+ applyVariant?: ((variant: SystemInstructionVariant, sessionId?: string) => void | Promise<void>) | undefined;
33
+ }
34
+ export interface SystemPromptVariantInfo {
35
+ variant: SystemInstructionVariant;
36
+ label: string;
37
+ hint: string;
38
+ /** Upper-bound token estimate of the identity block this variant injects. */
39
+ tokens: number;
40
+ }
41
+ export interface SystemPromptInfoPayload {
42
+ current: SystemInstructionVariant;
43
+ /** False until the user has explicitly picked a variant at least once. */
44
+ chosen: boolean;
45
+ variants: SystemPromptVariantInfo[];
46
+ /** Set when the token estimate could not be computed; variants still list. */
47
+ error?: string | undefined;
48
+ }
49
+ /**
50
+ * Compose the picker payload. Token counting reads the bundled/global/project
51
+ * instruction files, so a failure there (unreadable override dir, for instance)
52
+ * degrades to zero counts rather than denying the user the picker entirely.
53
+ */
54
+ /**
55
+ * The variant catalogue, plus which variant is live.
56
+ *
57
+ * The catalogue (labels, token costs, whether the user has ever chosen) is a
58
+ * project fact and the same for every tab. `current` is NOT: the identity
59
+ * variant is a per-session preference, so a tab that picked "lite" must not be
60
+ * told it is on the "pro" another tab just selected. `currentOverride` carries
61
+ * the asking session's own value; without one the config default stands, which
62
+ * is what a single-session host wants.
63
+ */
64
+ export declare function buildSystemPromptInfo(surface: SystemPromptSurface, currentOverride?: string | undefined): Promise<SystemPromptInfoPayload>;
65
+ /** Payload for a host that never wired the picker — the browser hides it. */
66
+ export declare function unavailableSystemPromptInfo(): SystemPromptInfoPayload;
67
+ //# sourceMappingURL=system-prompt-handlers.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Rebuild the live system prompt after something that feeds it changed.
3
+ *
4
+ * Two things can move underneath a running session: the active mode (which
5
+ * contributes a whole prompt layer) and the identity variant (Lite / Standard /
6
+ * Pro, which selects `system.md` vs `system-lite.md` vs `system-pro.md`). Both
7
+ * need the exact same eleven-dependency `DefaultSystemPromptBuilder` call, and
8
+ * a second copy of that call is a second place to forget `injectMemory: false`
9
+ * — which would double-inject SAGE memories, since its turn middleware is the
10
+ * single memory channel.
11
+ */
12
+ import { type Context, DefaultSystemPromptBuilder } from '@wrongstack/core/agent';
13
+ import { type Container } from '@wrongstack/core/kernel';
14
+ import type { ToolRegistry } from '@wrongstack/core/registry';
15
+ import type { Config, MemoryPort, ModeStore, SkillLoader } from '@wrongstack/core/types';
16
+ type ModelCapabilities = NonNullable<ConstructorParameters<typeof DefaultSystemPromptBuilder>[0]>['modelCapabilities'];
17
+ export interface SystemPromptRebuildDeps {
18
+ modeStore: ModeStore | undefined;
19
+ memoryStore: MemoryPort | undefined;
20
+ skillLoader: SkillLoader | undefined;
21
+ modelCapabilities: ModelCapabilities;
22
+ context: Context;
23
+ toolRegistry: ToolRegistry;
24
+ getConfig: () => Pick<Config, 'provider' | 'model' | 'systemPrompt' | 'features'>;
25
+ projectRoot: string;
26
+ globalRoot: string;
27
+ /**
28
+ * When supplied, the freshly built builder replaces the container-bound one.
29
+ *
30
+ * Matters for the identity variant specifically: the boot-time builder has
31
+ * the old variant baked into its `instructionPaths`, and everything that
32
+ * resolves `TOKENS.SystemPromptBuilder` later — the subagent factory, most
33
+ * visibly — would otherwise keep composing subagent prompts from the variant
34
+ * the user just moved away from.
35
+ */
36
+ container?: Container | undefined;
37
+ }
38
+ /**
39
+ * Recompose `context.systemPrompt` for `modeId` using the variant that context
40
+ * is running. Mutates the context in place; the caller decides what to
41
+ * broadcast afterwards.
42
+ */
43
+ export declare function rebuildSystemPrompt(deps: SystemPromptRebuildDeps, modeId: string): Promise<void>;
44
+ export {};
45
+ //# sourceMappingURL=system-prompt-rebuild.d.ts.map
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Terminal dashboard — the WebUI host's tidy console surface.
3
+ *
4
+ * The server process talks to the terminal through raw `console.*` calls
5
+ * scattered across dozens of modules (structured JSON warn/error lines,
6
+ * `[WebUI] …` progress chatter) and, on top of that, a live status block
7
+ * that used to be re-printed on every state change. With four tabs running
8
+ * those streams interleaved into an unreadable wall of text.
9
+ *
10
+ * This module is the single coordinator for both hosts (standalone server
11
+ * and the CLI-embedded `wstack --webui`):
12
+ *
13
+ * - **A fixed status panel at the bottom.** Session rows (running/idle,
14
+ * iteration, subagents, elapsed) are redrawn IN PLACE with ANSI cursor
15
+ * moves instead of appended, so the stats stay visible without growing
16
+ * the scrollback. `setSessions` is fingerprint-gated: identical rows
17
+ * never rewrite the panel.
18
+ *
19
+ * - **An ordered log stream above the panel.** Every forwarded
20
+ * `console.*` line erases the panel, prints with a `HH:MM:SS LEVEL`
21
+ * prefix, then redraws the panel — a log line can never land mid-panel.
22
+ * Structured JSON lines (the `{level,event,message,…}` shape the log
23
+ * shims emit) are rendered as readable single lines.
24
+ *
25
+ * - **Noise control.** Identical consecutive lines collapse into one
26
+ * `↑ previous line repeated N×` notice, and `quiet` mode keeps
27
+ * `log/info/debug` chatter out of the terminal entirely (ring-buffered
28
+ * for a failure dump) while `warn/error` still flow — the contract the
29
+ * CLI host's quiet surface used to own.
30
+ *
31
+ * Non-TTY output and `WEBUI_VERBOSE=1` bypass everything: redirected
32
+ * output and log collectors keep the complete append-only log.
33
+ */
34
+ /** Minimal console surface the dashboard can wrap (injectable for tests). */
35
+ export interface DashboardConsoleTarget {
36
+ log: (...args: unknown[]) => void;
37
+ info: (...args: unknown[]) => void;
38
+ warn: (...args: unknown[]) => void;
39
+ error: (...args: unknown[]) => void;
40
+ debug: (...args: unknown[]) => void;
41
+ }
42
+ /** Minimal stdout surface for panel drawing (injectable for tests). */
43
+ export interface DashboardStdout {
44
+ isTTY?: boolean | undefined;
45
+ columns?: number | undefined;
46
+ write(chunk: string): void;
47
+ }
48
+ /** One session row on the panel. Built by the live-status logger. */
49
+ export interface DashboardSessionRow {
50
+ id: string;
51
+ provider: string;
52
+ model: string;
53
+ isRunning: boolean;
54
+ iteration?: {
55
+ index: number;
56
+ max?: number | undefined;
57
+ } | undefined;
58
+ runningSubagents: number;
59
+ totalSubagents: number;
60
+ /** Epoch ms when the current run started, for the elapsed column. */
61
+ runningSince?: number | undefined;
62
+ agents?: readonly DashboardAgentRow[] | undefined;
63
+ }
64
+ /** One leader/subagent row nested under a session branch. */
65
+ export interface DashboardAgentRow {
66
+ id: string;
67
+ label: string;
68
+ provider: string;
69
+ model: string;
70
+ status: string;
71
+ iteration?: {
72
+ index: number;
73
+ max?: number | undefined;
74
+ } | undefined;
75
+ toolCalls: number;
76
+ /** Epoch ms while the agent is still active. */
77
+ startedAt?: number | undefined;
78
+ /** Final elapsed time for completed agents. */
79
+ durationMs?: number | undefined;
80
+ }
81
+ export interface TerminalDashboardOptions {
82
+ /** Panel title, e.g. `WebUI` / `SimpleUI`. */
83
+ title?: string | undefined;
84
+ /** Live URL shown in the panel header (token-less). */
85
+ getUrl?: (() => string | undefined) | undefined;
86
+ stdout?: DashboardStdout | undefined;
87
+ consoleTarget?: DashboardConsoleTarget | undefined;
88
+ /**
89
+ * Mute `log/info/debug` chatter into the ring buffer; only `warn/error`
90
+ * reach the terminal. The CLI-embedded host defaults this on (the browser
91
+ * owns the chatter), the standalone host off.
92
+ */
93
+ quiet?: boolean | undefined;
94
+ /** Pass EVERYTHING through untouched — the `WEBUI_VERBOSE=1` escape hatch. */
95
+ verbose?: boolean | undefined;
96
+ /** Ring-buffer size for {@link TerminalDashboard.recent}. */
97
+ bufferLines?: number | undefined;
98
+ /** Max session rows drawn before a `+N more` overflow line. */
99
+ maxRows?: number | undefined;
100
+ /** Max visible log lines kept above the live status tree. */
101
+ maxLogRows?: number | undefined;
102
+ now?: (() => number) | undefined;
103
+ }
104
+ export interface TerminalDashboard {
105
+ /** `false` when the console was left untouched (non-TTY or verbose). */
106
+ readonly enabled: boolean;
107
+ /** Chatter lines dropped from the terminal so far (quiet mode). */
108
+ readonly mutedCount: number;
109
+ /** Newest muted lines, oldest first — for a diagnostic dump on failure. */
110
+ recent(): readonly string[];
111
+ /** Redraw the status panel in place. No-op when the rows are unchanged. */
112
+ setSessions(rows: readonly DashboardSessionRow[]): void;
113
+ /** Erase the panel, flush pending notices, restore the console. Idempotent. */
114
+ stop(): void;
115
+ }
116
+ export declare function isWebUIVerboseEnv(env?: Record<string, string | undefined>): boolean;
117
+ /**
118
+ * Render one structured `{level,event,message,…}` JSON line into a body
119
+ * (event, message, `sess:` tag, scalar extras) plus the record's own level.
120
+ * The caller stamps the timestamp and level tag exactly once — returning the
121
+ * full line here would double-stamp. Returns `null` when the line is not a
122
+ * structured record, in which case the raw text is shown as-is.
123
+ */
124
+ export declare function prettifyStructuredLine(raw: string, colorize: boolean): {
125
+ level: string;
126
+ text: string;
127
+ } | null;
128
+ /**
129
+ * Build the panel lines for the current session rows. Pure — exported for
130
+ * tests. Lines are plain (no ANSI); the caller colorizes.
131
+ */
132
+ export declare function renderSessionPanelLines(rows: readonly DashboardSessionRow[], options: {
133
+ title: string;
134
+ url?: string | undefined;
135
+ maxRows?: number | undefined;
136
+ width?: number | undefined;
137
+ now?: number | undefined;
138
+ }): string[];
139
+ /**
140
+ * Start the dashboard. Disabled (console untouched, panel never drawn) on a
141
+ * non-TTY stdout or when `verbose` is set — redirected output and log
142
+ * collectors keep the complete append-only log.
143
+ */
144
+ export declare function startTerminalDashboard(options?: TerminalDashboardOptions): TerminalDashboard;
145
+ //# sourceMappingURL=terminal-dashboard.d.ts.map
@@ -1,10 +1,10 @@
1
- import type { WebSocket } from 'ws';
2
- import type { TrustBoundary } from '@wrongstack/core/security';
3
- import type { ConfigStore, ModelsRegistry, SecretVault, SessionStore } from '@wrongstack/core/types';
4
- import type { JournalEntry } from '@wrongstack/core/storage';
5
1
  import type { Agent, AgentPipelines } from '@wrongstack/core/agent';
6
2
  import type { EventBus } from '@wrongstack/core/kernel';
7
3
  import type { ToolRegistry } from '@wrongstack/core/registry';
4
+ import type { TrustBoundary } from '@wrongstack/core/security';
5
+ import type { JournalEntry } from '@wrongstack/core/storage';
6
+ import type { ConfigStore, ModelsRegistry, SecretVault, SessionStore } from '@wrongstack/core/types';
7
+ import type { WebSocket } from 'ws';
8
8
  /** Compatibility envelope; new boundaries should decode to CanonicalServerMessage. */
9
9
  export interface WSServerMessage {
10
10
  type: string;
@@ -84,6 +84,20 @@ export interface WebUIOptions {
84
84
  * webui an observer slot.
85
85
  */
86
86
  subscribeEternalIteration?: ((fn: (entry: JournalEntry) => void) => () => void) | undefined;
87
+ /**
88
+ * Stop every subagent that ONE session spawned.
89
+ *
90
+ * Aborting a session's run only unwinds the workers the leader is blocked
91
+ * on. Anything started with `spawn_subagent` + `assign_task` keeps running
92
+ * because nothing asked it to stop — so a tab's Stop button silenced the
93
+ * leader while its fleet ground on in the background. The host owns the
94
+ * Director, so it supplies the cascade; the webui server only knows WHICH
95
+ * session was stopped.
96
+ *
97
+ * Session-scoped by contract: with four tabs live, stopping one tab must
98
+ * never reach into another tab's fleet.
99
+ */
100
+ stopSessionFleet?: ((sessionId: string) => void | Promise<void>) | undefined;
87
101
  }
88
102
  export interface BackendServices {
89
103
  agent: Agent;
@@ -98,7 +112,22 @@ export interface BackendServices {
98
112
  }
99
113
  export interface ConnectedClient {
100
114
  ws: WebSocket;
115
+ /**
116
+ * The session this connection last acted on. Kept for the abort-on-close
117
+ * path and for surfaces that only ever show one session at a time.
118
+ */
101
119
  sessionId: string | null;
120
+ /**
121
+ * EVERY session this connection is displaying.
122
+ *
123
+ * One browser page holds up to four tabs on ONE socket, so `sessionId`
124
+ * alone describes only whichever tab the user last touched — filtering
125
+ * broadcasts by it dropped the other three tabs' runs before they left the
126
+ * server. The client declares its open set with `session.subscribe`; an
127
+ * empty set means "not declared", and such a client falls back to the
128
+ * single-session filter.
129
+ */
130
+ sessionIds?: Set<string> | undefined;
102
131
  connectedAt: number;
103
132
  /** Remote socket address captured for verbose rejection logging. */
104
133
  remoteAddress?: string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Live status tracker for the WebUI host terminal.
3
+ *
4
+ * Subscribes to the agent/iteration events, merges them with the host's
5
+ * session list, and feeds one {@link DashboardSessionRow} per session to the
6
+ * terminal dashboard's fixed panel — running/idle state, iteration counter,
7
+ * subagent counts and run elapsed time. Redraws are debounced and
8
+ * fingerprint-gated by the dashboard, so bursts of events cost at most one
9
+ * panel repaint, and the heartbeat only refreshes the elapsed column.
10
+ *
11
+ * When the dashboard is disabled (non-TTY output or `WEBUI_VERBOSE=1`) the
12
+ * tracker falls back to printing a compact status block on change — the
13
+ * append-only shape redirected output and log collectors expect.
14
+ */
15
+ import type { DashboardSessionRow, TerminalDashboard } from './terminal-dashboard.js';
16
+ interface EventBusLike {
17
+ on(event: string, listener: (...args: unknown[]) => void): unknown;
18
+ off(event: string, listener: (...args: unknown[]) => void): unknown;
19
+ }
20
+ export interface StatusSessionInfo {
21
+ id: string;
22
+ model: string;
23
+ provider: string;
24
+ isRunning: boolean;
25
+ }
26
+ interface SubagentRecord {
27
+ id: string;
28
+ role: string;
29
+ status: string;
30
+ provider?: string | undefined;
31
+ model?: string | undefined;
32
+ iteration?: {
33
+ index: number;
34
+ max?: number | undefined;
35
+ } | undefined;
36
+ toolCalls: number;
37
+ startedAt?: number | undefined;
38
+ durationMs?: number | undefined;
39
+ }
40
+ export interface WebUILiveStatusLoggerOptions {
41
+ events: EventBusLike;
42
+ getSessionList: () => StatusSessionInfo[];
43
+ /** The terminal dashboard that owns the fixed panel. */
44
+ dashboard: Pick<TerminalDashboard, 'enabled' | 'setSessions'>;
45
+ debounceMs?: number | undefined;
46
+ heartbeatMs?: number | undefined;
47
+ now?: (() => number) | undefined;
48
+ }
49
+ /**
50
+ * Merge the host session list with the tracked subagents, iterations and
51
+ * run-start timestamps into panel rows. Pure — exported for tests.
52
+ */
53
+ export declare function buildSessionRows(sessions: readonly StatusSessionInfo[], sessionSubagents: ReadonlyMap<string, Map<string, SubagentRecord>>, sessionIterations: ReadonlyMap<string, {
54
+ index: number;
55
+ max?: number | undefined;
56
+ }>, runStartedAt: ReadonlyMap<string, number>, sessionToolCalls?: ReadonlyMap<string, number>): DashboardSessionRow[];
57
+ /** Compact append-only block for non-TTY output (log files, pipes). */
58
+ export declare function formatSessionRowsBlock(rows: readonly DashboardSessionRow[]): string;
59
+ export declare function startWebUILiveStatusLogger(options: WebUILiveStatusLoggerOptions): () => void;
60
+ export {};
61
+ //# sourceMappingURL=webui-status-logger.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { WebSocket } from 'ws';
2
- import { type WorklistContext } from './handlers/worklist-handlers.js';
2
+ import { type WorklistContext, type WorklistMessage } from './handlers/worklist-handlers.js';
3
3
  import type { WSClientMessage } from './types.js';
4
4
  export interface WorklistRouteHandlers {
5
5
  getTodos: (ws: WebSocket, msg: WSClientMessage) => Promise<void> | void;
@@ -13,7 +13,13 @@ export interface WorklistRouteHandlers {
13
13
  updatePlanItem: (ws: WebSocket, msg: WSClientMessage) => Promise<void> | void;
14
14
  }
15
15
  export interface WorklistRouteContext {
16
- getContext: () => WorklistContext;
16
+ /**
17
+ * Resolve the worklist context for ONE request. The message is handed over
18
+ * so a multi-session host can bind to the session the payload names instead
19
+ * of whichever session the shared root context currently points at.
20
+ * Hosts that ignore it keep the single-context behaviour.
21
+ */
22
+ getContext: (message?: WorklistMessage) => WorklistContext;
17
23
  allowMessage?: ((ws: WebSocket, message: WSClientMessage) => boolean) | undefined;
18
24
  }
19
25
  export declare function createWorklistRouteHandlers(ctx: WorklistRouteContext): WorklistRouteHandlers;
@@ -0,0 +1,44 @@
1
+ import type { Agent, Context } from '@wrongstack/core/agent';
2
+ import type { WebSocket } from 'ws';
3
+ import type { WorklistContext, WorklistMessage } from './handlers/worklist-handlers.js';
4
+ import type { WSServerMessage } from './types.js';
5
+ /**
6
+ * Per-session binding for the worklist routes (todos / tasks / plan).
7
+ *
8
+ * The worklist used to read and write through the shared ROOT context — the
9
+ * one `session.resume` re-points to whichever tab was switched to last. A
10
+ * request stamped with a background tab's session id was therefore *allowed*
11
+ * by the session gate but served (and mutated!) the foreground session's
12
+ * board: every tab showed one shared list, and a click in tab B could rewrite
13
+ * tab A's todos.
14
+ *
15
+ * This factory resolves the context the request actually names:
16
+ *
17
+ * - untagged requests keep the root context (single-session hosts, older
18
+ * clients) — identical to the previous behaviour;
19
+ * - a request naming the root session keeps the root context;
20
+ * - a request naming another session resolves that session's agent
21
+ * (read-only `peekAgent` first, then `getAgent`, which creates one — the
22
+ * four-tab registry with idle eviction is the same one the conversation
23
+ * routes already use).
24
+ *
25
+ * Agents minted while the root sat on a different session inherit the root's
26
+ * meta verbatim, including its `plan.path` / `task.path` sidecar paths. Those
27
+ * are re-scoped to the requested session here, before any read or write can
28
+ * land in the neighbouring session's `.plan.json` / `.tasks.json`.
29
+ */
30
+ interface SessionWorklistDeps {
31
+ /** Shared root context — bound to whichever session the runtime last activated. */
32
+ rootContext: Context;
33
+ /** Agent for a session WITHOUT creating one; preferred for read paths. */
34
+ peekAgent?: ((sessionId?: string) => Agent | undefined) | undefined;
35
+ /** Agent for a session, creating one when absent (four-tab registry). */
36
+ getAgent?: ((sessionId?: string) => Agent) | undefined;
37
+ /** Sessions directory holding the per-session plan/task/todos sidecars. */
38
+ sessionsDir?: string | undefined;
39
+ send: (ws: WebSocket, msg: WSServerMessage) => void;
40
+ broadcast: (msg: WSServerMessage) => void;
41
+ }
42
+ export declare function createSessionAwareWorklistContext(deps: SessionWorklistDeps): (message?: WorklistMessage | undefined) => WorklistContext;
43
+ export {};
44
+ //# sourceMappingURL=worklist-session-context.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import type { EventBus } from '@wrongstack/core/kernel';
2
2
  import type { Logger } from '@wrongstack/core/types';
3
3
  import type { WebSocket } from 'ws';
4
- export interface WorktreeManagementDeps {
4
+ interface WorktreeManagementDeps {
5
5
  projectRoot: string;
6
6
  /** Board snapshot dir — powers the cross-process liveness guard on cleanup. */
7
7
  boardsDir: string;
@@ -100,4 +100,5 @@ export declare class WorktreeWebSocketHandler {
100
100
  private broadcast;
101
101
  private send;
102
102
  }
103
+ export {};
103
104
  //# sourceMappingURL=worktree-ws-handler.d.ts.map
@@ -10,9 +10,15 @@ interface ModelSwitchPayload {
10
10
  provider: string;
11
11
  model: string;
12
12
  requestId?: string | undefined;
13
+ /**
14
+ * The tab that asked. Every WebUI tab runs its own session with its own
15
+ * model, so a switch applies to the requesting session's context — not to
16
+ * whichever session the runtime last had in front.
17
+ */
18
+ sessionId?: string | undefined;
13
19
  }
14
20
  export declare function validateModelSwitchPayload(payload: unknown): PayloadValidationResult<ModelSwitchPayload>;
15
- export interface ModelFallbackChoicePayload {
21
+ interface ModelFallbackChoicePayload {
16
22
  requestId: string;
17
23
  providerId?: string | undefined;
18
24
  model?: string | undefined;
@@ -46,6 +52,15 @@ export interface MailboxSendPayload {
46
52
  body: string;
47
53
  priority: 'low' | 'normal' | 'high';
48
54
  replyTo?: string | undefined;
55
+ /**
56
+ * The tab this message was composed in.
57
+ *
58
+ * Carried because `to: 'leader'` is AMBIGUOUS once a WebUI page holds four
59
+ * sessions: every tab's agent is registered as a leader, so an unscoped
60
+ * message addressed that way is folded into whichever of them happens to
61
+ * poll the mailbox — a "btw" typed in tab 3 steering the run in tab 1.
62
+ */
63
+ sessionId?: string | undefined;
49
64
  }
50
65
  export declare function validateMailboxSendPayload(payload: unknown): PayloadValidationResult<MailboxSendPayload>;
51
66
  export declare function validateMailboxAgentsPayload(payload: unknown): PayloadValidationResult<MailboxAgentsPayload | undefined>;
@@ -137,6 +152,16 @@ interface GitDiffPayload {
137
152
  path: string;
138
153
  }
139
154
  export declare function validateGitDiffPayload(payload: unknown): PayloadValidationResult<GitDiffPayload>;
155
+ interface GitPathsPayload {
156
+ paths: string[];
157
+ }
158
+ export declare function validateGitStagePayload(payload: unknown): PayloadValidationResult<GitPathsPayload>;
159
+ export declare function validateGitUnstagePayload(payload: unknown): PayloadValidationResult<GitPathsPayload>;
160
+ export declare function validateGitDiscardPayload(payload: unknown): PayloadValidationResult<GitPathsPayload>;
161
+ interface GitCommitPayload {
162
+ message: string;
163
+ }
164
+ export declare function validateGitCommitPayload(payload: unknown): PayloadValidationResult<GitCommitPayload>;
140
165
  interface ProjectsAddPayload {
141
166
  root: string;
142
167
  name?: string;
@@ -160,7 +185,7 @@ export declare function validateProjectsSelectPayload(payload: unknown): Payload
160
185
  * configures. The spawn itself is hardened separately by
161
186
  * `buildWin32CmdShimInvocation` (CMDI-005).
162
187
  */
163
- export interface McpServerPayload {
188
+ interface McpServerPayload {
164
189
  name: string;
165
190
  [key: string]: unknown;
166
191
  }
@@ -14,11 +14,28 @@ export declare function sendSerialized(ws: WebSocket, data: string, frameBytes?:
14
14
  */
15
15
  export declare function send(ws: WebSocket, msg: object): void;
16
16
  /**
17
- * Broadcast a JSON message to every connected client.
18
- * Swallows per-socket send errors — a client that disconnected between the
19
- * readyState check and `ws.send()` is cleaned up by its own `close` handler.
17
+ * Broadcast a JSON message to connected clients.
18
+ *
19
+ * When a sessionId is present in msg.payload, only clients displaying that
20
+ * session receive it — see `clientWantsSession`. Global messages without a
21
+ * sessionId are sent to all connected clients.
22
+ */
23
+ export declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: object, targetSessionId?: string): void;
24
+ /**
25
+ * Does this connection display the session a message is addressed to?
26
+ *
27
+ * A page with four tabs open is ONE socket. Deciding delivery from
28
+ * `client.sessionId` — the tab last touched — silently dropped the other
29
+ * three tabs' runs at the wire, which looks exactly like "the background tab
30
+ * stopped working". `sessionIds` is the declared open set (`session.subscribe`);
31
+ * a connection that has not declared one keeps the old single-session filter,
32
+ * so surfaces that only ever show one session are unaffected.
20
33
  */
21
- export declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: object): void;
34
+ export declare function clientWantsSession(client: Pick<ConnectedClient, 'sessionId' | 'sessionIds'>, sessionId: string | undefined): boolean;
35
+ /**
36
+ * Broadcast unconditionally to all connected clients.
37
+ */
38
+ export declare function broadcastAll(clients: Map<WebSocket, ConnectedClient>, msg: object): void;
22
39
  /**
23
40
  * Send a success/failure result message (used by key.* and provider.* handlers).
24
41
  * The frontend expects `key.operation_result` with `{ success, message }`.
@@ -54,4 +71,12 @@ export declare function buildWebUIAccessUrl(opts: {
54
71
  publicUrl?: string | undefined;
55
72
  }): string;
56
73
  export declare function envFlag(name: string): boolean;
74
+ /**
75
+ * The session a client message names, or undefined. Session-scoped replies
76
+ * (`brain.status`, `/diag`) use it so the answer describes the tab that asked
77
+ * rather than whichever session the runtime happens to be on.
78
+ */
79
+ export declare function messageSessionId(msg: {
80
+ payload?: unknown;
81
+ }): string | undefined;
57
82
  //# sourceMappingURL=ws-utils.d.ts.map