@wrongstack/tui 0.298.3 → 0.299.0

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.
@@ -940,6 +940,24 @@ export type Action = {
940
940
  type: 'sidebarScroll';
941
941
  delta: number;
942
942
  viewportHeight?: number | undefined;
943
+ /**
944
+ * Rows occupied by routed twin panels (Todos / Plan / Kanban / …)
945
+ * mounted above `SidebarContent` inside `RightSidebar`. Subtracted
946
+ * from `viewportHeight` so the scroll clamp matches the actual
947
+ * viewport the user can scroll through. Defaults to 0 (no twins
948
+ * mounted) when omitted so existing call sites stay correct.
949
+ */
950
+ sidebarTwinRowCount?: number | undefined;
951
+ /**
952
+ * Whether the swarm panel is on the sidebar using the effective
953
+ * source (picker draft when the picker is open, persisted
954
+ * `liveSettings` otherwise — see `app-view.tsx`). Drives the
955
+ * mission-queue reservation in `computeMaxSidebarScroll`; without
956
+ * this field the reducer only sees the picker draft and under-
957
+ * reserves the mission card when a config-only 'sidebar' swarm
958
+ * mode is persisted but the picker has never been opened.
959
+ */
960
+ effectiveSwarmOnSidebar?: boolean | undefined;
943
961
  }
944
962
  /** Reset sidebar scroll offset to 0. */
945
963
  | {
@@ -51,6 +51,31 @@ interface AppKeyHandlerOptions {
51
51
  * scrollbar hit-tests use this instead of terminalColumns so the scrollbar
52
52
  * track is correctly positioned to the left of the sidebar. */
53
53
  mainColumnWidth: number;
54
+ /**
55
+ * Effective swarm-on-sidebar read: `panelPositions.fleet === 'sidebar'`
56
+ * OR the legacy `showAgentSwarmPanel === 'sidebar'` flag from
57
+ * `liveSettings`. The renderer at `app-view.tsx:897-899` reads
58
+ * ONLY the legacy `showAgentSwarmPanel` field (not the panel-
59
+ * position map) to decide whether to show the mission card; the
60
+ * scroll-clamp reservation must match that source or a config-only
61
+ * legacy 'sidebar' swarm mode (no recent picker open) will render
62
+ * the mission card but the clamp will under-reserve, hiding the
63
+ * bottom mission rows behind `RightSidebar`'s `overflowY="hidden"`
64
+ * viewport. The OR with `panelPositions.fleet` is directionally
65
+ * safe (over-reservation is harmless) and aligns with the field
66
+ * that gates whether the swarm twin is mounted. See
67
+ * {@link SidebarLayoutState.effectiveSwarmOnSidebar}. Threaded into
68
+ * `sidebarScroll` dispatches so the reducer's mission-queue
69
+ * reservation matches the actual render.
70
+ */
71
+ effectiveSwarmOnSidebar: boolean;
72
+ /**
73
+ * Approximate row count for routed sidebar twin panels mounted above
74
+ * `SidebarContent`. Subtracted from `viewportHeight` by the reducer's
75
+ * scroll clamp so the user can't scroll past the end into blank space.
76
+ * See {@link SidebarLayoutState.sidebarTwinRowCount}.
77
+ */
78
+ sidebarTwinRowCount: number;
54
79
  statusBarWrapRef: MutableRefObject<DOMElement | null>;
55
80
  belowStatusBarRef: MutableRefObject<DOMElement | null>;
56
81
  liveStatuslineMode: string;
@@ -1,10 +1,9 @@
1
1
  import type { FleetChatVerbosity, TokenSavingTier } from '@wrongstack/core/types';
2
- import type { CacheTtl, ContextMode, ReasoningEffort, StatuslineMode } from './settings-contracts.js';
2
+ import type { AgentSwarmPanelMode, CacheTtl, ContextMode, ReasoningEffort, StatuslineMode } from './settings-contracts.js';
3
3
  export type { PanelId, PanelPosition, PanelPositionMap } from './ui-contracts.js';
4
4
  export { coercePanelPositionMap, DEFAULT_PANEL_POSITIONS, PANEL_IDS, } from './ui-contracts.js';
5
5
  import type { PanelPositionMap } from './ui-contracts.js';
6
- /** Where to render the agent swarm panel: lower region, sidebar, or hidden. */
7
- export type AgentSwarmPanelMode = 'bottom' | 'sidebar' | 'off';
6
+ export type { AgentSwarmPanelMode } from './settings-contracts.js';
8
7
  /**
9
8
  * Coerce a persisted showAgentSwarmPanel value (which may be a legacy boolean
10
9
  * or the new tri-state string) into a valid AgentSwarmPanelMode.
@@ -18,6 +18,25 @@ export interface SidebarLayoutState {
18
18
  /** Width AppView must pass to nested routed sidebar panel frames. */
19
19
  sidebarContentWidth: number;
20
20
  mainColumnWidth: number;
21
+ /**
22
+ * Whether the swarm panel (`AgentSwarmPanelMode === 'sidebar'`) is
23
+ * effective on the right sidebar using the dual source: picker draft
24
+ * when `state.settingsPicker.open`, persisted `liveSettings.panelPositions`
25
+ * otherwise. Mirrors the renderer-side read in `app-view.tsx` so the
26
+ * scroll-clamp reducer can reserve the mission-queue rows even when
27
+ * the persisted config never went through the picker UI.
28
+ */
29
+ effectiveSwarmOnSidebar: boolean;
30
+ /**
31
+ * Approximate row count for routed sidebar twin panels mounted above
32
+ * `SidebarContent` inside `RightSidebar`. The reducer subtracts this
33
+ * from `viewportHeight` when computing the sidebar scroll clamp so the
34
+ * user can't scroll past the end into blank space when one or more
35
+ * routed twins are mounted. Each twin contributes a per-panel
36
+ * `ESTIMATED_HEIGHT` constant; over-estimation is safe (existing
37
+ * convention — see `SIDEBAR_MISSION_MAX_WRAP_LINES` docstring).
38
+ */
39
+ sidebarTwinRowCount: number;
21
40
  }
22
41
  /**
23
42
  * Resolve the TUI's right-sidebar layout once from the open-panel flags and
@@ -25,5 +44,38 @@ export interface SidebarLayoutState {
25
44
  * this so a sidebar-routed panel narrows history consistently instead of
26
45
  * being treated as a full-width overlay by one surface.
27
46
  */
28
- export declare function resolveSidebarLayout(state: State, termCols: number, panelPositionsInput: Partial<Record<PanelId, 'bottom' | 'sidebar'>> | undefined, mailboxPanelOpen: boolean): SidebarLayoutState;
47
+ export declare function resolveSidebarLayout(state: State, termCols: number, panelPositionsInput: Partial<Record<PanelId, 'bottom' | 'sidebar'>> | undefined, mailboxPanelOpen: boolean,
48
+ /**
49
+ * Open flags for each routable panel, used to compute the visible-twin
50
+ * row count for the scroll-clamp reservation. Panels not listed
51
+ * (or `undefined`) are treated as closed. Should mirror the same
52
+ * `sidebarPanelOpenFlags` map built in `app-view.tsx` so the
53
+ * dispatcher and the renderer agree on which twins are mounted.
54
+ */
55
+ sidebarOpenFlags?: Partial<Record<PanelId, boolean>> | undefined,
56
+ /**
57
+ * Legacy `showAgentSwarmPanel === 'sidebar'` flag from the persisted
58
+ * `liveSettings` config — the same field the renderer at
59
+ * `app-view.tsx:897-899` reads (picker draft when open, persisted
60
+ * `liveSettings.showAgentSwarmPanel` when closed) to decide whether
61
+ * the mission card renders on the sidebar. The scroll-clamp
62
+ * reservation must match this source or a config-only 'sidebar'
63
+ * swarm mode (no recent picker open) renders the mission card but
64
+ * the clamp under-reserves, hiding the bottom mission rows behind
65
+ * `RightSidebar`'s `overflowY="hidden"` viewport. OR'd with
66
+ * `panelPositions.fleet === 'sidebar'` for safety (over-reservation
67
+ * is harmless). Defaults to `false` when omitted so existing call
68
+ * sites stay correct.
69
+ */
70
+ legacySwarmOnSidebar?: boolean | undefined): SidebarLayoutState;
71
+ /**
72
+ * Conservative row-height estimate for any routed sidebar twin panel.
73
+ * Each twin renders a 1-row header + a 1-row separator + variable content +
74
+ * optional 1-row footer. 8 rows covers the natural height of the
75
+ * content-rich twins (Todos / Plan / Kanban / Goal / Sessions /
76
+ * Coordinator) without overshooting the simpler ones (Fleet / Worktree /
77
+ * etc.). Over-estimation is safe; the scroll-clamp subtracts this from
78
+ * `viewportHeight`.
79
+ */
80
+ export declare const ESTIMATED_TWIN_HEIGHT = 8;
29
81
  //# sourceMappingURL=app-ui-state.d.ts.map
@@ -7,6 +7,14 @@
7
7
  * persist to the global config).
8
8
  */
9
9
  import type { BrainRiskLevel } from './brain-contracts.js';
10
+ /** One selectable Council decision lens, as published by the host. */
11
+ export interface BrainPanelPersona {
12
+ id: string;
13
+ name: string;
14
+ description: string;
15
+ /** Seats using this lens get veto power unless the seat overrides it. */
16
+ defaultVeto?: boolean | undefined;
17
+ }
10
18
  /** One configured pool entry / council voter, display-mapped. */
11
19
  export interface BrainPanelVoter {
12
20
  label: string;
@@ -43,8 +51,26 @@ export interface BrainPanelSettings {
43
51
  usingSessionModel: boolean;
44
52
  councilEnabled: boolean;
45
53
  councilMinRisk: 'medium' | 'high' | 'critical';
54
+ /** Fraction of seats that must return a valid vote (undefined = default 0.5). */
55
+ councilQuorum?: number | undefined;
56
+ /** Fraction of cast weight the winner must exceed (undefined = default 0.5). */
57
+ councilApproval?: number | undefined;
58
+ /** Panel-diversity warning policy. A same-model panel agrees with itself. */
59
+ councilDistinctness: 'none' | 'model' | 'provider';
60
+ /** Per-seat completion timeout (undefined = inherit the decision timeout). */
61
+ councilPerCallTimeoutMs?: number | undefined;
62
+ /** Seats polled concurrently, 1..8 (undefined = default 3). */
63
+ councilMaxConcurrency?: number | undefined;
64
+ /** Output budget for the judge call (undefined = follows the seat budget). */
65
+ councilJudgeMaxTokens?: number | undefined;
46
66
  /** Explicitly configured voters (empty = seats derive from the pool). */
47
67
  voters: BrainPanelVoter[];
68
+ /**
69
+ * Selectable decision lenses, published by the host from the Council persona
70
+ * registry. Absent on hosts that predate the catalog — `personaCycle()` then
71
+ * falls back to the three lenses the panel used to hard-code.
72
+ */
73
+ personaCatalog?: BrainPanelPersona[] | undefined;
48
74
  /** Effective council seat labels (empty = council disabled). */
49
75
  councilSeats: string[];
50
76
  /**
@@ -110,6 +136,17 @@ export interface BrainPanelHost {
110
136
  toggleVoterVeto(index: number): Promise<string | null>;
111
137
  setJudge(providerId: string, model: string): Promise<string | null>;
112
138
  clearJudge(): Promise<string | null>;
139
+ setCouncilQuorum(fraction: number): Promise<string | null>;
140
+ setCouncilApproval(fraction: number): Promise<string | null>;
141
+ setCouncilDistinctness(mode: 'none' | 'model' | 'provider'): Promise<string | null>;
142
+ /**
143
+ * The three positive-integer council knobs. `undefined` clears back to the
144
+ * default — unlike the LLM/cache ladders, `BrainCouncilPatch` accepts `null`
145
+ * for these, so a "default" rung is reachable here.
146
+ */
147
+ setCouncilPerCallTimeout(ms: number | undefined): Promise<string | null>;
148
+ setCouncilMaxConcurrency(count: number | undefined): Promise<string | null>;
149
+ setCouncilJudgeMaxTokens(tokens: number | undefined): Promise<string | null>;
113
150
  setLedgerEnabled(on: boolean): Promise<string | null>;
114
151
  setAutoDeny(count: number | undefined): Promise<string | null>;
115
152
  setTerminalPolicy(policy: BrainTerminalPolicyValue): Promise<string | null>;
@@ -157,6 +194,18 @@ export type BrainPanelRow = {
157
194
  kind: 'voterAdd';
158
195
  } | {
159
196
  kind: 'judge';
197
+ } | {
198
+ kind: 'councilQuorum';
199
+ } | {
200
+ kind: 'councilApproval';
201
+ } | {
202
+ kind: 'councilDistinctness';
203
+ } | {
204
+ kind: 'councilTimeout';
205
+ } | {
206
+ kind: 'councilConcurrency';
207
+ } | {
208
+ kind: 'councilJudgeMaxTokens';
160
209
  } | {
161
210
  kind: 'ledgerToggle';
162
211
  } | {
@@ -224,7 +273,25 @@ export declare const LLM_MAX_TOKENS_PRESETS: readonly number[];
224
273
  export declare const LLM_MIN_CONFIDENCE_PRESETS: readonly number[];
225
274
  export declare const CACHE_TTL_PRESETS: readonly number[];
226
275
  export declare const CACHE_MAX_ENTRIES_PRESETS: readonly number[];
276
+ /**
277
+ * Council ladders. `undefined` is the "default" rung — `BrainCouncilPatch`
278
+ * accepts `null` for these three, unlike the LLM/cache knobs.
279
+ */
280
+ export declare const COUNCIL_FRACTION_PRESETS: readonly number[];
281
+ export declare const COUNCIL_DISTINCTNESS_PRESETS: ReadonlyArray<'none' | 'model' | 'provider'>;
282
+ export declare const COUNCIL_TIMEOUT_PRESETS: ReadonlyArray<number | undefined>;
283
+ export declare const COUNCIL_CONCURRENCY_PRESETS: ReadonlyArray<number | undefined>;
284
+ export declare const COUNCIL_JUDGE_MAX_TOKENS_PRESETS: ReadonlyArray<number | undefined>;
285
+ /**
286
+ * Fallback lens cycle for hosts that publish no `personaCatalog`. Prefer
287
+ * {@link personaCycle}, which uses the host's catalog when it is available —
288
+ * the registry ships six lenses, not these three.
289
+ */
227
290
  export declare const PERSONA_CYCLE: readonly ['executor', 'skeptic', 'auditor'];
291
+ /** Lens ids the panel may cycle through for the given settings snapshot. */
292
+ export declare function personaCycle(settings: BrainPanelSettings): readonly string[];
293
+ /** Human-readable lens name for a persona id, falling back to the id itself. */
294
+ export declare function personaLabel(settings: BrainPanelSettings, id: string | undefined): string;
228
295
  /** Cycle helper: step through a preset ladder from the current value. */
229
296
  export declare function cyclePreset<T>(presets: ReadonlyArray<T>, current: T, delta: number): T;
230
297
  //# sourceMappingURL=brain-panel-model.d.ts.map
@@ -1,6 +1,21 @@
1
1
  import type { TodoItem } from '@wrongstack/core/agent';
2
2
  import React from 'react';
3
3
  import type { HistoryEntry } from './types.js';
4
+ /**
5
+ * One-line summary of how a council resolved.
6
+ *
7
+ * `distinctTargetCount` is on the headline on purpose: a panel where several
8
+ * seats resolved to the SAME model produces a perfectly normal-looking verdict
9
+ * while adding cost without adding independence, and that is invisible unless
10
+ * the count is shown next to the seat count.
11
+ */
12
+ export declare function councilHeadline(council: NonNullable<Extract<HistoryEntry, {
13
+ kind: 'brain';
14
+ }>['council']>): string;
15
+ /** One seat's line under the council headline. */
16
+ export declare function councilSeatLine(seat: NonNullable<Extract<HistoryEntry, {
17
+ kind: 'brain';
18
+ }>['council']>['seats'][number]): string;
4
19
  export declare const Entry: React.MemoExoticComponent<({ entry, termWidth, termHeight, setSuggestions, autonomyMode, multiDiffSummaryThreshold, todos, showModelReasoning, showSageMemoryInject, }: {
5
20
  entry: HistoryEntry;
6
21
  termWidth: number;
@@ -1,4 +1,4 @@
1
- import { SettingsPickerPatch } from '../settings-contracts.js';
1
+ import type { SettingsPickerPatch } from '../settings-contracts.js';
2
2
  /** Selectable presets for the auto-proceed delay, so the field is fully
3
3
  * keyboard-cyclable (←/→) instead of needing typed numeric input. */
4
4
  export declare const DELAY_PRESETS_MS: number[];
@@ -43,5 +43,21 @@ export interface SidebarContentProps {
43
43
  /** The current session ID — used to highlight the active session row. */
44
44
  currentSessionId?: string | undefined;
45
45
  }
46
+ /**
47
+ * Normalized "is this session row the current session?" predicate used by
48
+ * both the `SidebarContent` SESSIONS card (live + resume rows) and the
49
+ * `SessionsPanelSidebar` (sidebar twin). The runtime `currentSessionId`
50
+ * is the authoritative source — when defined, the row whose id matches
51
+ * is the current row. When undefined (e.g. before the host registers a
52
+ * session), the per-row `isCurrent` flag is the fallback (carried over
53
+ * from the resume-picker domain for the case where the picker marks
54
+ * the current session as non-resumable but the host hasn't published
55
+ * the runtime id yet). A row without an id can never be the current
56
+ * row, regardless of what its fallback flag says. The two SESSIONS
57
+ * rows used to disagree on which key to read (live used strict id
58
+ * compare, resume used `rs.isCurrent` first); this helper is the
59
+ * single source of truth.
60
+ */
61
+ export declare function isCurrentSession(rowId: string | undefined, currentSessionId: string | undefined, fallbackIsCurrent?: boolean | undefined): boolean;
46
62
  export declare function SidebarContent({ contextWindow, entries, fleetCounts, provider, model, width, scrollOffset, focused, todos, showSwarmSection, liveSessions, resumeSessions, currentSessionId, }: SidebarContentProps): React.ReactElement;
47
63
  //# sourceMappingURL=sidebar-content.d.ts.map
@@ -1,4 +1,37 @@
1
1
  /** Render-neutral conversation entry contracts retained by TUI state. */
2
+ /** One seat's observable vote. No hidden chain-of-thought is retained. */
3
+ export interface BrainCouncilSeatTrace {
4
+ seatId: string;
5
+ persona: string;
6
+ status: 'valid' | 'invalid' | 'failed' | 'cancelled';
7
+ /** The option the seat voted for, when the question was option-bearing. */
8
+ optionId?: string | undefined;
9
+ model?: string | undefined;
10
+ veto?: boolean | undefined;
11
+ durationMs?: number | undefined;
12
+ error?: string | undefined;
13
+ }
14
+ /**
15
+ * Panel summary attached to a council-tier Brain entry.
16
+ *
17
+ * The council is by far the most expensive tier — N provider calls per
18
+ * decision — and used to be entirely invisible: the votes were emitted onto
19
+ * the bus but no surface consumed them, so a slow, costly decision looked
20
+ * identical to a free policy one.
21
+ */
22
+ export interface BrainCouncilTrace {
23
+ resolution: string;
24
+ configuredSeatCount: number;
25
+ validVoteCount: number;
26
+ /** Distinct provider/model targets that served the valid votes. */
27
+ distinctTargetCount: number;
28
+ judgeUsed: boolean;
29
+ totalTokens?: number | undefined;
30
+ durationMs?: number | undefined;
31
+ /** Structural warnings — most importantly a CORRELATED (non-diverse) panel. */
32
+ warnings?: string[] | undefined;
33
+ seats: BrainCouncilSeatTrace[];
34
+ }
2
35
  export interface AutonomyAgentStatus {
3
36
  /** Short display name (e.g. "Brain", "Shadow", "Kanban", "Mailbox", "Memory"). */
4
37
  name: string;
@@ -185,6 +218,12 @@ export type HistoryEntry = {
185
218
  rationale?: string | undefined;
186
219
  outcome?: string | undefined;
187
220
  interventionKind?: string | undefined;
221
+ /**
222
+ * How the multi-LLM council reached this verdict, when a council decided
223
+ * it. Present only for council-tier decisions; the single-LLM and policy
224
+ * tiers leave it undefined.
225
+ */
226
+ council?: BrainCouncilTrace | undefined;
188
227
  } | {
189
228
  id: number;
190
229
  kind: 'banner';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { Settings } from './app.js';
2
2
  export { buildGoalPreamble } from './app.js';
3
3
  export type { AuthCatalogRow, AuthFlowIo, AuthFlowResult, AuthKeyRow, AuthLocalPresetRow, AuthOAuthKind, AuthPanelHost, AuthProviderRow, } from './auth-panel-model.js';
4
- export type { BrainDenyIsTerminal, BrainHeuristicKey, BrainPanelHeuristics, BrainPanelHost, BrainPanelSettings, BrainPanelVoter, BrainTerminalPolicyValue, BrainTraceContent, } from './brain-panel-model.js';
4
+ export type { BrainDenyIsTerminal, BrainHeuristicKey, BrainPanelHeuristics, BrainPanelHost, BrainPanelPersona, BrainPanelSettings, BrainPanelVoter, BrainTerminalPolicyValue, BrainTraceContent, } from './brain-panel-model.js';
5
5
  export { replaySessionMessages } from './components/history/replay.js';
6
6
  export type { PluginPickerItem } from './components/plugin-picker.js';
7
7
  export type { ParsedNextStep, ParseNextStepsResult } from './components/suggestions.js';
package/dist/index.js CHANGED
@@ -3827,6 +3827,14 @@ function brainPanelRows(settings) {
3827
3827
  rows.push({ kind: "councilMinRisk" });
3828
3828
  for (let i = 0; i < voters.length; i += 1) rows.push({ kind: "voter", index: i });
3829
3829
  rows.push({ kind: "voterAdd" }, { kind: "judge" });
3830
+ rows.push(
3831
+ { kind: "councilQuorum" },
3832
+ { kind: "councilApproval" },
3833
+ { kind: "councilDistinctness" },
3834
+ { kind: "councilTimeout" },
3835
+ { kind: "councilConcurrency" },
3836
+ { kind: "councilJudgeMaxTokens" }
3837
+ );
3830
3838
  }
3831
3839
  rows.push({ kind: "ledgerToggle" });
3832
3840
  if (settings.ledgerEnabled) rows.push({ kind: "autoDeny" });
@@ -3876,6 +3884,39 @@ var CACHE_TTL_PRESETS = [
3876
3884
  36e5
3877
3885
  ];
3878
3886
  var CACHE_MAX_ENTRIES_PRESETS = [50, 100, 200, 500, 1e3];
3887
+ var COUNCIL_FRACTION_PRESETS = [0.25, 0.5, 0.6, 0.67, 0.75, 1];
3888
+ var COUNCIL_DISTINCTNESS_PRESETS = [
3889
+ "none",
3890
+ "model",
3891
+ "provider"
3892
+ ];
3893
+ var COUNCIL_TIMEOUT_PRESETS = [
3894
+ void 0,
3895
+ 1e4,
3896
+ 15e3,
3897
+ 3e4,
3898
+ 6e4
3899
+ ];
3900
+ var COUNCIL_CONCURRENCY_PRESETS = [
3901
+ void 0,
3902
+ 1,
3903
+ 2,
3904
+ 3,
3905
+ 4,
3906
+ 6,
3907
+ 8
3908
+ ];
3909
+ var COUNCIL_JUDGE_MAX_TOKENS_PRESETS = [
3910
+ void 0,
3911
+ 300,
3912
+ 500,
3913
+ 700,
3914
+ 1200
3915
+ ];
3916
+ function personaLabel(settings, id) {
3917
+ if (!id) return "voter";
3918
+ return settings.personaCatalog?.find((persona) => persona.id === id)?.name ?? id;
3919
+ }
3879
3920
  function cyclePreset(presets, current, delta) {
3880
3921
  const idx = presets.indexOf(current);
3881
3922
  const from = idx >= 0 ? idx : 0;
@@ -7257,6 +7298,50 @@ function useBrainPanel(opts) {
7257
7298
  runBrainMutation(() => host.setCouncilMinRisk(next));
7258
7299
  return;
7259
7300
  }
7301
+ case "councilQuorum":
7302
+ runBrainMutation(
7303
+ () => host.setCouncilQuorum(
7304
+ cyclePreset(COUNCIL_FRACTION_PRESETS, settings.councilQuorum ?? 0.5, delta)
7305
+ )
7306
+ );
7307
+ return;
7308
+ case "councilApproval":
7309
+ runBrainMutation(
7310
+ () => host.setCouncilApproval(
7311
+ cyclePreset(COUNCIL_FRACTION_PRESETS, settings.councilApproval ?? 0.5, delta)
7312
+ )
7313
+ );
7314
+ return;
7315
+ case "councilDistinctness": {
7316
+ const next = cycleEnum(
7317
+ COUNCIL_DISTINCTNESS_PRESETS,
7318
+ settings.councilDistinctness,
7319
+ delta
7320
+ );
7321
+ runBrainMutation(() => host.setCouncilDistinctness(next));
7322
+ return;
7323
+ }
7324
+ case "councilTimeout":
7325
+ runBrainMutation(
7326
+ () => host.setCouncilPerCallTimeout(
7327
+ cyclePreset(COUNCIL_TIMEOUT_PRESETS, settings.councilPerCallTimeoutMs, delta)
7328
+ )
7329
+ );
7330
+ return;
7331
+ case "councilConcurrency":
7332
+ runBrainMutation(
7333
+ () => host.setCouncilMaxConcurrency(
7334
+ cyclePreset(COUNCIL_CONCURRENCY_PRESETS, settings.councilMaxConcurrency, delta)
7335
+ )
7336
+ );
7337
+ return;
7338
+ case "councilJudgeMaxTokens":
7339
+ runBrainMutation(
7340
+ () => host.setCouncilJudgeMaxTokens(
7341
+ cyclePreset(COUNCIL_JUDGE_MAX_TOKENS_PRESETS, settings.councilJudgeMaxTokens, delta)
7342
+ )
7343
+ );
7344
+ return;
7260
7345
  case "ledgerToggle":
7261
7346
  runBrainMutation(() => host.setLedgerEnabled(!settings.ledgerEnabled));
7262
7347
  return;
@@ -14626,7 +14711,8 @@ function reduceSettingsValues(state, action) {
14626
14711
  }
14627
14712
 
14628
14713
  // src/reducers/workspace-panels.ts
14629
- function computeMaxSidebarScroll(state, viewportHeight = 20) {
14714
+ var SIDEBAR_MISSION_MAX_WRAP_LINES = 10;
14715
+ function computeMaxSidebarScroll(state, viewportHeight = 20, effectiveSwarmOnSidebar) {
14630
14716
  let contentHeight = 0;
14631
14717
  contentHeight += 4;
14632
14718
  contentHeight += 3;
@@ -14644,9 +14730,9 @@ function computeMaxSidebarScroll(state, viewportHeight = 20) {
14644
14730
  if (runningSubagents.length > agentCap) contentHeight += 1;
14645
14731
  contentHeight += 1;
14646
14732
  }
14647
- const swarmMode = state.settingsPicker.showAgentSwarmPanel;
14648
- if (swarmMode === "sidebar") {
14649
- contentHeight += 1 + SIDEBAR_MISSION_ROWS + 1 + 1;
14733
+ const swarmOnSidebar = effectiveSwarmOnSidebar ?? state.settingsPicker.showAgentSwarmPanel === "sidebar";
14734
+ if (swarmOnSidebar) {
14735
+ contentHeight += 1 + SIDEBAR_MISSION_ROWS * SIDEBAR_MISSION_MAX_WRAP_LINES + 1 + 1;
14650
14736
  }
14651
14737
  const liveSessionCount = state.sessionsPanel.sessions.length;
14652
14738
  const resumeSessionCount = state.resumePicker.sessions.length;
@@ -14742,9 +14828,11 @@ function reduceWorkspacePanels(state, action) {
14742
14828
  case "toggleSidebarFocus":
14743
14829
  return state.sidebarFocused ? { ...state, sidebarFocused: false, sidebarScrollOffset: 0 } : { ...state, sidebarFocused: true };
14744
14830
  case "sidebarScroll": {
14831
+ const adjustedViewportHeight = (action.viewportHeight ?? 20) - (action.sidebarTwinRowCount ?? 0);
14745
14832
  const maxScroll = computeMaxSidebarScroll(
14746
14833
  state,
14747
- action.viewportHeight
14834
+ Math.max(1, adjustedViewportHeight),
14835
+ action.effectiveSwarmOnSidebar
14748
14836
  );
14749
14837
  return {
14750
14838
  ...state,
@@ -19161,7 +19249,7 @@ import { useEffect as useEffect41 } from "react";
19161
19249
  var MONITOR_INTERVENTION_GRACE_MS = 5e3;
19162
19250
  function useBrainEvents(events, dispatch, getSessionId) {
19163
19251
  useEffect41(() => {
19164
- const isCurrentSession = (sessionId) => {
19252
+ const isCurrentSession2 = (sessionId) => {
19165
19253
  const current = getSessionId?.();
19166
19254
  return !sessionId || !current || sessionId === current;
19167
19255
  };
@@ -19176,8 +19264,21 @@ function useBrainEvents(events, dispatch, getSessionId) {
19176
19264
  return decision.reason;
19177
19265
  }
19178
19266
  };
19267
+ const councilTraces = /* @__PURE__ */ new Map();
19268
+ const councilVotes = /* @__PURE__ */ new Map();
19269
+ const COUNCIL_TRACE_MAX = 100;
19270
+ const capCouncilMap = (map) => {
19271
+ while (map.size > COUNCIL_TRACE_MAX) {
19272
+ const oldest = map.keys().next().value;
19273
+ if (oldest === void 0) break;
19274
+ map.delete(oldest);
19275
+ }
19276
+ };
19179
19277
  const addBrainEntry = (status, payload) => {
19180
19278
  const p = payload;
19279
+ const council = councilTraces.get(p.request.id);
19280
+ councilTraces.delete(p.request.id);
19281
+ councilVotes.delete(p.request.id);
19181
19282
  const decision = decisionSummary(p.decision);
19182
19283
  dispatch({ type: "brainStatus", state: status, source: p.request.source, risk: p.request.risk, summary: decision });
19183
19284
  if (status === "ask_human") {
@@ -19194,7 +19295,7 @@ function useBrainEvents(events, dispatch, getSessionId) {
19194
19295
  dispatch({ type: "brainPromptClear" });
19195
19296
  }
19196
19297
  const rationale = p.decision.type === "deny" ? void 0 : p.decision.rationale;
19197
- dispatch({ type: "addEntry", entry: { kind: "brain", status, source: p.request.source, risk: p.request.risk, question: p.request.question, decision, rationale } });
19298
+ dispatch({ type: "addEntry", entry: { kind: "brain", status, source: p.request.source, risk: p.request.risk, question: p.request.question, decision, rationale, ...council ? { council } : {} } });
19198
19299
  };
19199
19300
  const pendingMonitorAnswers = /* @__PURE__ */ new Map();
19200
19301
  const SHOWN_MONITOR_ANSWERS_MAX = 500;
@@ -19207,11 +19308,11 @@ function useBrainEvents(events, dispatch, getSessionId) {
19207
19308
  }
19208
19309
  };
19209
19310
  const offRequested = events.on("brain.decision_requested", ({ sessionId, request }) => {
19210
- if (!isCurrentSession(sessionId)) return;
19311
+ if (!isCurrentSession2(sessionId)) return;
19211
19312
  dispatch({ type: "brainStatus", state: "deciding", source: request.source, risk: request.risk, summary: requestSummary(request) });
19212
19313
  });
19213
19314
  const offAnswered = events.on("brain.decision_answered", (payload) => {
19214
- if (!isCurrentSession(payload.sessionId)) return;
19315
+ if (!isCurrentSession2(payload.sessionId)) return;
19215
19316
  if (!payload.request.id.startsWith("brainmon-")) {
19216
19317
  addBrainEntry("answered", payload);
19217
19318
  return;
@@ -19220,7 +19321,7 @@ function useBrainEvents(events, dispatch, getSessionId) {
19220
19321
  if (previous) clearTimeout(previous);
19221
19322
  const timer = setTimeout(() => {
19222
19323
  pendingMonitorAnswers.delete(payload.request.id);
19223
- if (isCurrentSession(payload.sessionId)) {
19324
+ if (isCurrentSession2(payload.sessionId)) {
19224
19325
  rememberShownAnswer(payload.request.id);
19225
19326
  addBrainEntry("answered", payload);
19226
19327
  }
@@ -19229,13 +19330,52 @@ function useBrainEvents(events, dispatch, getSessionId) {
19229
19330
  pendingMonitorAnswers.set(payload.request.id, timer);
19230
19331
  });
19231
19332
  const offAskHuman = events.on("brain.decision_ask_human", (payload) => {
19232
- if (isCurrentSession(payload.sessionId)) addBrainEntry("ask_human", payload);
19333
+ if (isCurrentSession2(payload.sessionId)) addBrainEntry("ask_human", payload);
19233
19334
  });
19234
19335
  const offDenied = events.on("brain.decision_denied", (payload) => {
19235
- if (isCurrentSession(payload.sessionId)) addBrainEntry("denied", payload);
19336
+ if (isCurrentSession2(payload.sessionId)) addBrainEntry("denied", payload);
19337
+ });
19338
+ const offCouncilVote = events.on("brain.council_vote", (payload) => {
19339
+ if (!isCurrentSession2(payload.sessionId)) return;
19340
+ const seats = councilVotes.get(payload.requestId) ?? [];
19341
+ seats.push({
19342
+ seatId: payload.seatId,
19343
+ persona: payload.persona,
19344
+ status: payload.status,
19345
+ optionId: payload.optionId,
19346
+ model: payload.model,
19347
+ veto: payload.veto,
19348
+ durationMs: payload.durationMs,
19349
+ error: payload.error
19350
+ });
19351
+ councilVotes.set(payload.requestId, seats);
19352
+ capCouncilMap(councilVotes);
19353
+ dispatch({
19354
+ type: "brainStatus",
19355
+ state: "deciding",
19356
+ source: "council",
19357
+ risk: "high",
19358
+ summary: `council \xB7 ${seats.length} seat${seats.length === 1 ? "" : "s"} voted`
19359
+ });
19360
+ });
19361
+ const offCouncilResolved = events.on("brain.council_resolved", (payload) => {
19362
+ if (!isCurrentSession2(payload.sessionId)) return;
19363
+ councilTraces.set(payload.requestId, {
19364
+ resolution: payload.resolution,
19365
+ configuredSeatCount: payload.configuredSeatCount,
19366
+ validVoteCount: payload.validVoteCount,
19367
+ distinctTargetCount: payload.distinctTargetCount,
19368
+ judgeUsed: payload.judgeUsed,
19369
+ totalTokens: payload.usage?.totalTokens,
19370
+ durationMs: payload.usage?.durationMs,
19371
+ ...payload.warnings?.length ? { warnings: [...payload.warnings] } : {},
19372
+ seats: councilVotes.get(payload.requestId) ?? []
19373
+ });
19374
+ capCouncilMap(councilTraces);
19375
+ councilVotes.delete(payload.requestId);
19236
19376
  });
19237
19377
  const offIntervention = events.on("brain.intervention", (payload) => {
19238
- if (!isCurrentSession(payload.sessionId)) return;
19378
+ if (!isCurrentSession2(payload.sessionId)) return;
19239
19379
  if (shownMonitorAnswers.has(payload.request.id)) return;
19240
19380
  const pending = pendingMonitorAnswers.get(payload.request.id);
19241
19381
  if (pending) {
@@ -19265,9 +19405,13 @@ function useBrainEvents(events, dispatch, getSessionId) {
19265
19405
  offAnswered();
19266
19406
  offAskHuman();
19267
19407
  offDenied();
19408
+ offCouncilVote();
19409
+ offCouncilResolved();
19268
19410
  offIntervention();
19269
19411
  for (const timer of pendingMonitorAnswers.values()) clearTimeout(timer);
19270
19412
  pendingMonitorAnswers.clear();
19413
+ councilTraces.clear();
19414
+ councilVotes.clear();
19271
19415
  };
19272
19416
  }, [events, dispatch, getSessionId]);
19273
19417
  }
@@ -19324,13 +19468,13 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19324
19468
  );
19325
19469
  useEffect42(() => {
19326
19470
  const seen = /* @__PURE__ */ new Set();
19327
- const isCurrentSession = (sessionId) => {
19471
+ const isCurrentSession2 = (sessionId) => {
19328
19472
  const current = getSessionId?.();
19329
19473
  return !sessionId || !current || sessionId === current;
19330
19474
  };
19331
19475
  const mode = () => getChatMode?.() ?? "full";
19332
19476
  const offSpawned = events.on("subagent.spawned", (e) => {
19333
- if (!isCurrentSession(e.sessionId)) return;
19477
+ if (!isCurrentSession2(e.sessionId)) return;
19334
19478
  if (sessionGenerationRef) gate.track(e.subagentId);
19335
19479
  seen.add(e.subagentId);
19336
19480
  const l = lbl(e.subagentId, e.name);
@@ -19358,7 +19502,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19358
19502
  }
19359
19503
  });
19360
19504
  const offStarted = events.on("subagent.task_started", (e) => {
19361
- if (!isCurrentSession(e.sessionId)) return;
19505
+ if (!isCurrentSession2(e.sessionId)) return;
19362
19506
  if (!gate.isLive(e.subagentId)) return;
19363
19507
  seen.add(e.subagentId);
19364
19508
  const l = lbl(e.subagentId);
@@ -19378,7 +19522,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19378
19522
  }
19379
19523
  });
19380
19524
  const offCompleted = events.on("subagent.task_completed", (e) => {
19381
- if (!isCurrentSession(e.sessionId)) return;
19525
+ if (!isCurrentSession2(e.sessionId)) return;
19382
19526
  if (!gate.isLive(e.subagentId)) return;
19383
19527
  seen.add(e.subagentId);
19384
19528
  const l = lbl(e.subagentId);
@@ -19412,7 +19556,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19412
19556
  }
19413
19557
  });
19414
19558
  const offRemoved = events.on("subagent.removed", (e) => {
19415
- if (!isCurrentSession(e.sessionId)) return;
19559
+ if (!isCurrentSession2(e.sessionId)) return;
19416
19560
  seen.delete(e.subagentId);
19417
19561
  labelsRef.current.delete(e.subagentId);
19418
19562
  ctxDispatchRef.current.delete(e.subagentId);
@@ -19425,7 +19569,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19425
19569
  dispatch({ type: "fleetRemove", id: e.subagentId });
19426
19570
  });
19427
19571
  const offBudgetWarning = events.on("subagent.budget_warning", (e) => {
19428
- if (!isCurrentSession(e.sessionId)) return;
19572
+ if (!isCurrentSession2(e.sessionId)) return;
19429
19573
  if (!gate.isLive(e.subagentId)) return;
19430
19574
  seen.add(e.subagentId);
19431
19575
  const l = lbl(e.subagentId);
@@ -19453,7 +19597,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19453
19597
  }
19454
19598
  });
19455
19599
  const offBudgetExtended = events.on("subagent.budget_extended", (e) => {
19456
- if (!isCurrentSession(e.sessionId)) return;
19600
+ if (!isCurrentSession2(e.sessionId)) return;
19457
19601
  if (!gate.isLive(e.subagentId)) return;
19458
19602
  seen.add(e.subagentId);
19459
19603
  const l = lbl(e.subagentId);
@@ -19477,7 +19621,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19477
19621
  }
19478
19622
  });
19479
19623
  const offIterationSummary = events.on("subagent.iteration_summary", (e) => {
19480
- if (!isCurrentSession(e.sessionId)) return;
19624
+ if (!isCurrentSession2(e.sessionId)) return;
19481
19625
  if (!gate.isLive(e.subagentId)) return;
19482
19626
  seen.add(e.subagentId);
19483
19627
  if (mode() !== "full") return;
@@ -19497,7 +19641,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19497
19641
  });
19498
19642
  });
19499
19643
  const offCtxPct = events.on("subagent.ctx_pct", (e) => {
19500
- if (!isCurrentSession(e.sessionId)) return;
19644
+ if (!isCurrentSession2(e.sessionId)) return;
19501
19645
  if (!gate.isLive(e.subagentId)) return;
19502
19646
  seen.add(e.subagentId);
19503
19647
  const emit = () => {
@@ -19538,13 +19682,13 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19538
19682
  });
19539
19683
  const offConcurrencyChanged = events.on("concurrency.changed", (e) => {
19540
19684
  const { n, sessionId } = e;
19541
- if (!isCurrentSession(sessionId)) return;
19685
+ if (!isCurrentSession2(sessionId)) return;
19542
19686
  if (typeof n === "number" && n > 0) {
19543
19687
  dispatch({ type: "fleetConcurrency", n });
19544
19688
  }
19545
19689
  });
19546
19690
  const offLeaderCtxPct = events.on("ctx.pct", (e) => {
19547
- if (!isCurrentSession(e.sessionId)) return;
19691
+ if (!isCurrentSession2(e.sessionId)) return;
19548
19692
  latestLeaderCtxPctRef.current = e;
19549
19693
  const emit = () => {
19550
19694
  const latest = latestLeaderCtxPctRef.current ?? e;
@@ -19581,7 +19725,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19581
19725
  emit();
19582
19726
  });
19583
19727
  const offLeaderMaxContext = events.on("ctx.max_context", (e) => {
19584
- if (!isCurrentSession(e.sessionId)) return;
19728
+ if (!isCurrentSession2(e.sessionId)) return;
19585
19729
  if (e.maxContext > 0) setActiveMaxContext(e.maxContext);
19586
19730
  });
19587
19731
  const offCompactionFired = events.on("compaction.fired", (e) => {
@@ -19589,7 +19733,7 @@ function useSubagentEvents(events, dispatch, setActiveMaxContext, getSessionId,
19589
19733
  if (entry) dispatch({ type: "addEntry", entry });
19590
19734
  });
19591
19735
  const offTool = events.on("subagent.tool_executed", (e) => {
19592
- if (!isCurrentSession(e.sessionId)) return;
19736
+ if (!isCurrentSession2(e.sessionId)) return;
19593
19737
  if (!gate.isLive(e.subagentId)) return;
19594
19738
  dispatch({
19595
19739
  type: "fleetTool",
@@ -19648,12 +19792,12 @@ function useTuiEventBridge({
19648
19792
  }
19649
19793
  function useSessionEvents(events, dispatch, onClearHistory, getSessionId) {
19650
19794
  useEffect43(() => {
19651
- const isCurrentSession = (sessionId) => {
19795
+ const isCurrentSession2 = (sessionId) => {
19652
19796
  const current = getSessionId?.();
19653
19797
  return !sessionId || !current || sessionId === current;
19654
19798
  };
19655
19799
  const offCheckpoint = events.on("checkpoint.written", (e) => {
19656
- if (!isCurrentSession(e.sessionId)) return;
19800
+ if (!isCurrentSession2(e.sessionId)) return;
19657
19801
  dispatch({
19658
19802
  type: "checkpointReceived",
19659
19803
  cp: {
@@ -19665,7 +19809,7 @@ function useSessionEvents(events, dispatch, onClearHistory, getSessionId) {
19665
19809
  });
19666
19810
  });
19667
19811
  const offRewound = events.on("session.rewound", (e) => {
19668
- if (!isCurrentSession(e.sessionId)) return;
19812
+ if (!isCurrentSession2(e.sessionId)) return;
19669
19813
  dispatch({ type: "sessionRewound", toPromptIndex: e.toPromptIndex });
19670
19814
  dispatch({ type: "clearHistory" });
19671
19815
  dispatch({ type: "resetContextChip" });
@@ -19680,13 +19824,13 @@ function useSessionEvents(events, dispatch, onClearHistory, getSessionId) {
19680
19824
  function useGoalEvents(subscribeGoal, dispatch, stateRef, getSessionId) {
19681
19825
  useEffect43(() => {
19682
19826
  if (!subscribeGoal) return;
19683
- const isCurrentSession = (sessionId) => {
19827
+ const isCurrentSession2 = (sessionId) => {
19684
19828
  const current = getSessionId?.();
19685
19829
  return !sessionId || !current || sessionId === current;
19686
19830
  };
19687
19831
  const handler = (event, payload) => {
19688
19832
  const sessionId = payload && typeof payload === "object" && "sessionId" in payload ? payload.sessionId : void 0;
19689
- if (!isCurrentSession(sessionId)) return;
19833
+ if (!isCurrentSession2(sessionId)) return;
19690
19834
  switch (event) {
19691
19835
  case "phase.started": {
19692
19836
  const p = payload;
@@ -21216,19 +21360,31 @@ function mergeStatuslineHiddenItems(hookHidden, reducerHidden) {
21216
21360
  function isPickerOverlayOpen(state) {
21217
21361
  return state.modelPicker.open || state.autonomyPicker.open || state.modePicker.open || state.designPicker.open || state.resumePicker.open || state.promptPicker.open || state.settingsPicker.open || state.slashPicker.open || state.statuslinePicker.open || state.pluginPicker.open || state.mcpPicker.open || state.toolsPicker.open || state.brainPanel.open || state.helpPanel.open || state.shadowPanel.open || state.fKeyPicker.open || state.authPanel.open || state.picker.open;
21218
21362
  }
21219
- function resolveSidebarLayout(state, termCols, panelPositionsInput, mailboxPanelOpen) {
21363
+ function resolveSidebarLayout(state, termCols, panelPositionsInput, mailboxPanelOpen, sidebarOpenFlags, legacySwarmOnSidebar) {
21220
21364
  const panelPositions = coercePanelPositionMap(panelPositionsInput);
21221
21365
  const routedToSidebar = (id) => panelPositions[id] === "sidebar";
21222
21366
  const overlayOpen = isPickerOverlayOpen(state) || state.coordinator.monitorOpen && !routedToSidebar("coordinator") || state.auditPanelOpen || state.connectionsPanelOpen && !routedToSidebar("connections") || state.helpOpen || state.agentsMonitorOpen && !routedToSidebar("agents") || state.monitorOpen && !routedToSidebar("fleet") || state.contextPanelOpen || state.processListOpen && !routedToSidebar("processList") || state.todosMonitorOpen && !routedToSidebar("todos") || state.worktreeMonitorOpen && !routedToSidebar("worktree") || state.planPanelOpen && !routedToSidebar("plan") || state.kanbanPanelOpen && !routedToSidebar("kanban") || state.queuePanelOpen && !routedToSidebar("queue") || state.goalPanelOpen && !routedToSidebar("goal") || state.sessionsPanelOpen && !routedToSidebar("sessions") || state.projectPicker.open && !routedToSidebar("projectPicker") || state.goalKanbanPanelOpen || state.cronMonitorOpen || state.rewindOverlay != null || state.shellCommandWarning != null || state.confirmQueue.length > 0 || state.clearConfirm != null || state.exitConfirm != null || state.slashConfirm != null || state.escConfirm != null || state.sendModePicker != null || state.enhance != null || state.enhanceBusy || state.refineCountdown != null || state.refineFailure != null || state.continueConfirm != null || state.brainPrompt != null || mailboxPanelOpen || (state.goalRun?.monitorOpen ?? false) && !routedToSidebar("coordinator") || (state.sddBoard?.monitorOpen ?? false);
21223
21367
  const sidebarWidth = overlayOpen ? 0 : computeSidebarWidth(termCols);
21368
+ const effectiveSwarmOnSidebar = panelPositions.fleet === "sidebar" || (legacySwarmOnSidebar ?? false);
21369
+ let sidebarTwinRowCount = 0;
21370
+ if (sidebarOpenFlags) {
21371
+ for (const id of PANEL_IDS) {
21372
+ if (routedToSidebar(id) && sidebarOpenFlags[id]) {
21373
+ sidebarTwinRowCount += ESTIMATED_TWIN_HEIGHT;
21374
+ }
21375
+ }
21376
+ }
21224
21377
  return {
21225
21378
  panelPositions,
21226
21379
  overlayOpen,
21227
21380
  sidebarWidth,
21228
21381
  sidebarContentWidth: computeSidebarContentWidth(sidebarWidth),
21229
- mainColumnWidth: termCols - sidebarWidth
21382
+ mainColumnWidth: termCols - sidebarWidth,
21383
+ effectiveSwarmOnSidebar,
21384
+ sidebarTwinRowCount
21230
21385
  };
21231
21386
  }
21387
+ var ESTIMATED_TWIN_HEIGHT = 8;
21232
21388
 
21233
21389
  // src/app-view.tsx
21234
21390
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
@@ -26595,10 +26751,11 @@ function rowText(row, s2) {
26595
26751
  return { label: " risk floor", value: s2.councilMinRisk, dim: "council convenes at/above this risk" };
26596
26752
  case "voter": {
26597
26753
  const v = s2.voters[row.index];
26754
+ const lens = personaLabel(s2, v?.persona);
26598
26755
  return {
26599
26756
  label: ` ${row.index + 1}.`,
26600
26757
  value: v?.label ?? "",
26601
- dim: `${v?.persona ?? "voter"}${v?.veto ? " \xB7 VETO" : ""}${v?.weight ? ` \xB7 w=${v.weight}` : ""} (p persona \xB7 v veto \xB7 d remove)`
26758
+ dim: `${lens}${v?.veto ? " \xB7 VETO" : ""}${v?.weight ? ` \xB7 w=${v.weight}` : ""} (p persona \xB7 v veto \xB7 d remove)`
26602
26759
  };
26603
26760
  }
26604
26761
  case "voterAdd":
@@ -26611,6 +26768,42 @@ function rowText(row, s2) {
26611
26768
  dim: "Enter = pick \xB7 d = auto"
26612
26769
  };
26613
26770
  }
26771
+ case "councilQuorum":
26772
+ return {
26773
+ label: " quorum",
26774
+ value: String(s2.councilQuorum ?? 0.5),
26775
+ dim: "fraction of seats that must return a valid vote"
26776
+ };
26777
+ case "councilApproval":
26778
+ return {
26779
+ label: " approval",
26780
+ value: String(s2.councilApproval ?? 0.5),
26781
+ dim: "winning weight must exceed this fraction, else the judge decides"
26782
+ };
26783
+ case "councilDistinctness":
26784
+ return {
26785
+ label: " distinctness",
26786
+ value: s2.councilDistinctness,
26787
+ dim: s2.councilDistinctness === "none" ? "off \u2014 a same-model panel agrees with itself and is never reported" : `warn when seats do not use distinct ${s2.councilDistinctness}s`
26788
+ };
26789
+ case "councilTimeout":
26790
+ return {
26791
+ label: " seat timeout",
26792
+ value: s2.councilPerCallTimeoutMs === void 0 ? "default" : `${s2.councilPerCallTimeoutMs}ms`,
26793
+ dim: "per-seat completion budget (default = the decision timeout)"
26794
+ };
26795
+ case "councilConcurrency":
26796
+ return {
26797
+ label: " concurrency",
26798
+ value: s2.councilMaxConcurrency === void 0 ? "default (3)" : String(s2.councilMaxConcurrency),
26799
+ dim: "seats polled at once, 1..8 \u2014 higher is faster and burstier"
26800
+ };
26801
+ case "councilJudgeMaxTokens":
26802
+ return {
26803
+ label: " judge tokens",
26804
+ value: s2.councilJudgeMaxTokens === void 0 ? "default" : String(s2.councilJudgeMaxTokens),
26805
+ dim: "output budget for the tie-breaker call"
26806
+ };
26614
26807
  case "ledgerToggle":
26615
26808
  return {
26616
26809
  label: "Ledger",
@@ -32853,7 +33046,8 @@ function parseInline(text) {
32853
33046
  let urlEnd = i + 8;
32854
33047
  while (urlEnd < text.length) {
32855
33048
  const c = text[urlEnd];
32856
- if (c !== void 0 && c.match(/[a-zA-Z0-9._~:/?#@!$&+,;=%\-\[\]()'*]/)) {
33049
+ const urlChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._~:/?#@!" + String.fromCharCode(36) + String.fromCharCode(38) + "+,;=-%[]()'*";
33050
+ if (c !== void 0 && urlChars.includes(c)) {
32857
33051
  urlEnd++;
32858
33052
  } else {
32859
33053
  break;
@@ -33475,6 +33669,22 @@ function brainStatusStyle(status) {
33475
33669
  return { icon: "\xD7", color: "red" };
33476
33670
  }
33477
33671
  }
33672
+ function councilHeadline(council) {
33673
+ const parts = [
33674
+ `\u21B3 Council: ${council.resolution}`,
33675
+ `${council.validVoteCount}/${council.configuredSeatCount} seats`,
33676
+ `${council.distinctTargetCount} distinct target${council.distinctTargetCount === 1 ? "" : "s"}`
33677
+ ];
33678
+ if (council.judgeUsed) parts.push("judge used");
33679
+ if (council.durationMs !== void 0) parts.push(`${Math.round(council.durationMs / 100) / 10}s`);
33680
+ if (council.totalTokens) parts.push(`${council.totalTokens} tok`);
33681
+ return parts.join(" \xB7 ");
33682
+ }
33683
+ function councilSeatLine(seat) {
33684
+ const verdict = seat.status === "valid" ? seat.optionId ?? "stance" : seat.error ?? seat.status;
33685
+ const suffix = [seat.model, seat.veto ? "veto" : void 0].filter(Boolean).join(", ");
33686
+ return ` ${seat.status === "valid" ? "\u2022" : "\xD7"} ${seat.persona} \u2192 ${verdict}${suffix ? ` (${suffix})` : ""}`;
33687
+ }
33478
33688
  function memoryLifecycleStyle(action) {
33479
33689
  if (action === "entered" || action === "recovered") return { icon: "\u21B3", color: "green" };
33480
33690
  if (action === "exited") return { icon: "\u21B2", color: "red" };
@@ -34039,7 +34249,12 @@ var Entry = React8.memo(function Entry2({
34039
34249
  entry.outcome ? /* @__PURE__ */ jsx71(Box, { paddingLeft: indentWidth, marginTop: 1, children: /* @__PURE__ */ jsxs68(Text, { children: [
34040
34250
  /* @__PURE__ */ jsx71(Text, { bold: true, color: statusStyle.color, children: "\u2713 Outcome: " }),
34041
34251
  /* @__PURE__ */ jsx71(Text, { color: theme.textPrimary, children: entry.outcome })
34042
- ] }) }) : null
34252
+ ] }) }) : null,
34253
+ entry.council ? /* @__PURE__ */ jsxs68(Box, { flexDirection: "column", paddingLeft: indentWidth, marginTop: 1, children: [
34254
+ /* @__PURE__ */ jsx71(Text, { dimColor: true, children: councilHeadline(entry.council) }),
34255
+ entry.council.seats.map((seat) => /* @__PURE__ */ jsx71(Text, { dimColor: true, children: councilSeatLine(seat) }, seat.seatId)),
34256
+ (entry.council.warnings ?? []).map((warning) => /* @__PURE__ */ jsx71(Text, { color: theme.warn, children: ` \u26A0 ${warning}` }, warning))
34257
+ ] }) : null
34043
34258
  ]
34044
34259
  }
34045
34260
  );
@@ -34943,8 +35158,10 @@ var ScrollableHistory = memo3(function ScrollableHistory2({
34943
35158
  Box,
34944
35159
  {
34945
35160
  flexDirection: "column",
34946
- flexGrow: 1,
35161
+ width: termWidth,
35162
+ flexShrink: 0,
34947
35163
  height: vp,
35164
+ overflowX: "hidden",
34948
35165
  overflowY: "hidden",
34949
35166
  justifyContent: scrolled ? "flex-start" : "flex-end",
34950
35167
  children: /* @__PURE__ */ jsxs71(Box, { flexDirection: "column", flexShrink: 0, marginTop: scrolled && clip2 > 0 ? -clip2 : 0, children: [
@@ -35021,6 +35238,13 @@ function trunc2(s2, max) {
35021
35238
  if (s2.length <= max) return s2;
35022
35239
  return `${s2.slice(0, Math.max(1, max - 1))}\u2026`;
35023
35240
  }
35241
+ function isCurrentSession(rowId, currentSessionId, fallbackIsCurrent) {
35242
+ if (rowId === void 0) return false;
35243
+ if (currentSessionId !== void 0) {
35244
+ return rowId === currentSessionId;
35245
+ }
35246
+ return fallbackIsCurrent === true;
35247
+ }
35024
35248
  function statusGlyph(entry) {
35025
35249
  switch (entry.status) {
35026
35250
  case "running":
@@ -35055,8 +35279,7 @@ function liveSessionColor(status) {
35055
35279
  if (status === "active" || status === "running") return theme.success;
35056
35280
  if (status === "idle") return theme.accent;
35057
35281
  if (status === "error" || status === "stale") return theme.error;
35058
- if (status === "closing") return theme.warn;
35059
- return theme.textMuted;
35282
+ return theme.warn;
35060
35283
  }
35061
35284
  function outcomeBadge(outcome) {
35062
35285
  switch (outcome) {
@@ -35120,7 +35343,7 @@ function Card({
35120
35343
  }
35121
35344
  );
35122
35345
  }
35123
- function buildMissionRows(todos, maxRows, textWidth) {
35346
+ function buildMissionRows(todos, maxRows) {
35124
35347
  const list = todos ?? [];
35125
35348
  const total = list.length;
35126
35349
  const done = list.filter((t) => t.status === "completed").length;
@@ -35131,10 +35354,7 @@ function buildMissionRows(todos, maxRows, textWidth) {
35131
35354
  const rows = shown.map((t) => ({
35132
35355
  id: t.id,
35133
35356
  status: t.status,
35134
- label: trunc2(
35135
- t.status === "in_progress" && t.activeForm ? t.activeForm : t.content,
35136
- textWidth
35137
- )
35357
+ label: t.status === "in_progress" && t.activeForm ? t.activeForm : t.content
35138
35358
  }));
35139
35359
  return { rows, overflow: ordered.length - shown.length, done, total };
35140
35360
  }
@@ -35172,7 +35392,7 @@ function SidebarContent({
35172
35392
  ];
35173
35393
  const hiddenAgentCount = runningSubagents.length - agentCap;
35174
35394
  const running = fleetCounts?.running ?? runningSubagents.length;
35175
- const mission = showSwarmSection ? buildMissionRows(todos, SIDEBAR_MISSION_ROWS, innerWidth - 3) : { rows: [], overflow: 0, done: 0, total: 0 };
35395
+ const mission = showSwarmSection ? buildMissionRows(todos, SIDEBAR_MISSION_ROWS) : { rows: [], overflow: 0, done: 0, total: 0 };
35176
35396
  return /* @__PURE__ */ jsxs72(
35177
35397
  Box,
35178
35398
  {
@@ -35308,29 +35528,29 @@ function SidebarContent({
35308
35528
  ),
35309
35529
  mission.rows.map((m) => {
35310
35530
  if (m.status === "completed") {
35311
- return /* @__PURE__ */ jsxs72(Box, { children: [
35531
+ return /* @__PURE__ */ jsxs72(Box, { width: innerWidth, flexDirection: "row", children: [
35312
35532
  /* @__PURE__ */ jsxs72(Text, { color: theme.success, children: [
35313
35533
  glyphs.success,
35314
35534
  " "
35315
35535
  ] }),
35316
- /* @__PURE__ */ jsx75(Text, { color: theme.textMuted, dimColor: true, strikethrough: true, wrap: "truncate", children: m.label })
35536
+ /* @__PURE__ */ jsx75(Text, { color: theme.textMuted, dimColor: true, strikethrough: true, children: m.label })
35317
35537
  ] }, m.id);
35318
35538
  }
35319
35539
  if (m.status === "in_progress") {
35320
- return /* @__PURE__ */ jsxs72(Box, { children: [
35540
+ return /* @__PURE__ */ jsxs72(Box, { width: innerWidth, flexDirection: "row", children: [
35321
35541
  /* @__PURE__ */ jsxs72(Text, { color: theme.accent, children: [
35322
35542
  glyphs.running,
35323
35543
  " "
35324
35544
  ] }),
35325
- /* @__PURE__ */ jsx75(Text, { color: theme.textPrimary, bold: true, wrap: "truncate", children: m.label })
35545
+ /* @__PURE__ */ jsx75(Text, { color: theme.textPrimary, bold: true, children: m.label })
35326
35546
  ] }, m.id);
35327
35547
  }
35328
- return /* @__PURE__ */ jsxs72(Box, { children: [
35548
+ return /* @__PURE__ */ jsxs72(Box, { width: innerWidth, flexDirection: "row", children: [
35329
35549
  /* @__PURE__ */ jsxs72(Text, { color: theme.textMuted, children: [
35330
35550
  glyphs.pending,
35331
35551
  " "
35332
35552
  ] }),
35333
- /* @__PURE__ */ jsx75(Text, { color: theme.textSecondary, wrap: "truncate", children: m.label })
35553
+ /* @__PURE__ */ jsx75(Text, { color: theme.textSecondary, children: m.label })
35334
35554
  ] }, m.id);
35335
35555
  }),
35336
35556
  mission.overflow > 0 ? /* @__PURE__ */ jsxs72(Text, { color: theme.textMuted, dimColor: true, children: [
@@ -35353,7 +35573,10 @@ function SidebarContent({
35353
35573
  }
35354
35574
  ),
35355
35575
  liveSessions && liveSessions.length > 0 ? /* @__PURE__ */ jsx75(Box, { flexDirection: "column", children: liveSessions.slice(0, 3).map((s2) => {
35356
- const isCurrent = s2.sessionId === currentSessionId;
35576
+ const isCurrent = isCurrentSession(
35577
+ s2.sessionId,
35578
+ currentSessionId
35579
+ );
35357
35580
  const icon = isCurrent ? "\u25CF" : liveSessionIcon(s2.status);
35358
35581
  const color = liveSessionColor(s2.status);
35359
35582
  const name = trunc2(s2.projectName, innerWidth - 6);
@@ -35381,6 +35604,11 @@ function SidebarContent({
35381
35604
  flexDirection: "column",
35382
35605
  marginTop: liveSessions && liveSessions.length > 0 ? 1 : 0,
35383
35606
  children: resumeSessions.slice(0, 3).map((rs) => {
35607
+ const isCurrent = isCurrentSession(
35608
+ rs.id,
35609
+ currentSessionId,
35610
+ rs.isCurrent
35611
+ );
35384
35612
  const badge = outcomeBadge(rs.outcome);
35385
35613
  const title = trunc2(
35386
35614
  rs.title || rs.lastUserMessage || rs.id,
@@ -35395,9 +35623,9 @@ function SidebarContent({
35395
35623
  /* @__PURE__ */ jsx75(
35396
35624
  Text,
35397
35625
  {
35398
- color: rs.isCurrent ? theme.accent : theme.textSecondary,
35626
+ color: isCurrent ? theme.accent : theme.textSecondary,
35399
35627
  wrap: "truncate",
35400
- ...rs.isCurrent ? { bold: true } : {},
35628
+ bold: isCurrent,
35401
35629
  children: title
35402
35630
  }
35403
35631
  ),
@@ -35888,7 +36116,7 @@ function PlanPanelSidebar({
35888
36116
  ordered.length === 0 ? /* @__PURE__ */ jsx77(Text, { color: theme.textMuted, children: "no plan items" }) : ordered.map((item) => {
35889
36117
  const icon = item.status === "done" ? glyphs.success : item.status === "in_progress" ? "\u25D0" : "\u25CB";
35890
36118
  const color = item.status === "done" ? theme.success : item.status === "in_progress" ? theme.warn : theme.textMuted;
35891
- return /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
36119
+ return /* @__PURE__ */ jsxs74(Box, { width: inner, flexDirection: "row", children: [
35892
36120
  /* @__PURE__ */ jsx77(Text, { color, children: icon }),
35893
36121
  /* @__PURE__ */ jsxs74(
35894
36122
  Text,
@@ -35897,7 +36125,7 @@ function PlanPanelSidebar({
35897
36125
  ...item.status === "done" ? { dimColor: true, strikethrough: true } : {},
35898
36126
  children: [
35899
36127
  " ",
35900
- trunc3(item.title, inner - 4)
36128
+ item.title
35901
36129
  ]
35902
36130
  }
35903
36131
  )
@@ -35935,7 +36163,7 @@ function TodosPanelSidebar({ todos, width }) {
35935
36163
  const icon = t.status === "completed" ? glyphs.success : t.status === "in_progress" ? "\u25CF" : "\u25CB";
35936
36164
  const color = t.status === "completed" ? theme.success : t.status === "in_progress" ? theme.accent : theme.textMuted;
35937
36165
  const label = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
35938
- return /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
36166
+ return /* @__PURE__ */ jsxs74(Box, { width: inner, flexDirection: "row", children: [
35939
36167
  /* @__PURE__ */ jsx77(Text, { color, children: icon }),
35940
36168
  /* @__PURE__ */ jsxs74(
35941
36169
  Text,
@@ -35944,7 +36172,7 @@ function TodosPanelSidebar({ todos, width }) {
35944
36172
  ...t.status === "completed" ? { dimColor: true, strikethrough: true } : {},
35945
36173
  children: [
35946
36174
  " ",
35947
- trunc3(label, inner - 4)
36175
+ label
35948
36176
  ]
35949
36177
  }
35950
36178
  )
@@ -35965,7 +36193,7 @@ function QueuePanelSidebar({ items, width }) {
35965
36193
  kicker: "queued prompts",
35966
36194
  right: /* @__PURE__ */ jsx77(Text, { color: items.length > 0 ? theme.warn : theme.textMuted, bold: true, children: items.length }),
35967
36195
  footer: "F7 details",
35968
- children: /* @__PURE__ */ jsx77(SidebarPanelCard, { innerWidth: inner, marginBottom: 0, children: items.length === 0 ? /* @__PURE__ */ jsx77(Text, { color: theme.textMuted, children: "queue is empty" }) : items.slice(0, 10).map((item, i) => /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
36196
+ children: /* @__PURE__ */ jsx77(SidebarPanelCard, { innerWidth: inner, marginBottom: 0, children: items.length === 0 ? /* @__PURE__ */ jsx77(Text, { color: theme.textMuted, children: "queue is empty" }) : items.slice(0, 10).map((item, i) => /* @__PURE__ */ jsxs74(Box, { width: inner, flexDirection: "row", children: [
35969
36197
  /* @__PURE__ */ jsxs74(Text, { color: theme.textMuted, children: [
35970
36198
  i + 1,
35971
36199
  "."
@@ -35974,10 +36202,9 @@ function QueuePanelSidebar({ items, width }) {
35974
36202
  Text,
35975
36203
  {
35976
36204
  color: theme.textPrimary,
35977
- wrap: "truncate",
35978
36205
  children: [
35979
36206
  " ",
35980
- trunc3(item.displayText, inner - 6)
36207
+ item.displayText
35981
36208
  ]
35982
36209
  }
35983
36210
  )
@@ -36085,7 +36312,7 @@ function GoalPanelSidebar({
36085
36312
  ),
36086
36313
  deliverables.slice(0, 6).map((d, i) => {
36087
36314
  const done = /^\[[x✓]\]|✅|\(done\)/i.test(d);
36088
- return /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
36315
+ return /* @__PURE__ */ jsxs74(Box, { width: inner, flexDirection: "row", children: [
36089
36316
  /* @__PURE__ */ jsx77(Text, { color: done ? theme.success : theme.textMuted, children: done ? glyphs.success : glyphs.pending }),
36090
36317
  /* @__PURE__ */ jsxs74(
36091
36318
  Text,
@@ -36094,7 +36321,7 @@ function GoalPanelSidebar({
36094
36321
  ...done ? { dimColor: true, strikethrough: true } : {},
36095
36322
  children: [
36096
36323
  " ",
36097
- trunc3(d.replace(/^\[[ x✓]\]\s*/, ""), inner - 4)
36324
+ d.replace(/^\[[ x✓]\]\s*/, "")
36098
36325
  ]
36099
36326
  }
36100
36327
  )
@@ -36138,7 +36365,10 @@ function SessionsPanelSidebar({
36138
36365
  }
36139
36366
  ),
36140
36367
  live.map((s2) => {
36141
- const isCurrent = s2.sessionId === currentSessionId;
36368
+ const isCurrent = isCurrentSession(
36369
+ s2.sessionId,
36370
+ currentSessionId
36371
+ );
36142
36372
  const icon = isCurrent ? "\u25CF" : liveSessionGlyph(s2.status);
36143
36373
  const color = liveSessionColor2(s2.status);
36144
36374
  return /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
@@ -36171,13 +36401,18 @@ function SessionsPanelSidebar({
36171
36401
  const rel = fmtRelative2(rs.lastActivityAt ?? rs.endedAt);
36172
36402
  const outcomeGlyph = rs.outcome === "completed" ? glyphs.success : rs.outcome === "error" ? glyphs.failure : rs.outcome === "timeout" ? "\u23F1" : "\u2298";
36173
36403
  const outcomeColor = rs.outcome === "completed" ? theme.success : rs.outcome === "error" ? theme.error : rs.outcome === "timeout" ? theme.warn : theme.textMuted;
36404
+ const isCurrent = isCurrentSession(
36405
+ rs.id,
36406
+ currentSessionId,
36407
+ rs.isCurrent
36408
+ );
36174
36409
  return /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
36175
36410
  /* @__PURE__ */ jsx77(Text, { color: outcomeColor, children: outcomeGlyph }),
36176
36411
  /* @__PURE__ */ jsxs74(
36177
36412
  Text,
36178
36413
  {
36179
- color: rs.isCurrent ? theme.accent : theme.textSecondary,
36180
- bold: rs.isCurrent === true,
36414
+ color: isCurrent ? theme.accent : theme.textSecondary,
36415
+ bold: isCurrent,
36181
36416
  wrap: "truncate",
36182
36417
  children: [
36183
36418
  " ",
@@ -36295,11 +36530,11 @@ function KanbanPanelSidebar({
36295
36530
  innerWidth: inner
36296
36531
  }
36297
36532
  ),
36298
- activeCardTitles.length === 0 ? /* @__PURE__ */ jsx77(Text, { color: theme.textMuted, children: "no active cards" }) : activeCardTitles.slice(0, 6).map((title, i) => /* @__PURE__ */ jsxs74(Box, { flexDirection: "row", children: [
36533
+ activeCardTitles.length === 0 ? /* @__PURE__ */ jsx77(Text, { color: theme.textMuted, children: "no active cards" }) : activeCardTitles.slice(0, 6).map((title, i) => /* @__PURE__ */ jsxs74(Box, { width: inner, flexDirection: "row", children: [
36299
36534
  /* @__PURE__ */ jsx77(Text, { color: theme.warn, children: "\u25CF" }),
36300
- /* @__PURE__ */ jsxs74(Text, { color: theme.textPrimary, wrap: "truncate", children: [
36535
+ /* @__PURE__ */ jsxs74(Text, { color: theme.textPrimary, children: [
36301
36536
  " ",
36302
- trunc3(title, inner - 4)
36537
+ title
36303
36538
  ] })
36304
36539
  ] }, i))
36305
36540
  ] })
@@ -39164,6 +39399,8 @@ function createAppKeyHandler(options) {
39164
39399
  terminalColumns,
39165
39400
  terminalRows,
39166
39401
  mainColumnWidth,
39402
+ effectiveSwarmOnSidebar,
39403
+ sidebarTwinRowCount,
39167
39404
  statusBarWrapRef,
39168
39405
  belowStatusBarRef,
39169
39406
  liveStatuslineMode,
@@ -39366,11 +39603,23 @@ function createAppKeyHandler(options) {
39366
39603
  if (state.sidebarFocused && !overlayOpen) {
39367
39604
  const sidebarViewportHeight = termRows - 2;
39368
39605
  if (key.upArrow) {
39369
- dispatch({ type: "sidebarScroll", delta: -1, viewportHeight: sidebarViewportHeight });
39606
+ dispatch({
39607
+ type: "sidebarScroll",
39608
+ delta: -1,
39609
+ viewportHeight: sidebarViewportHeight,
39610
+ sidebarTwinRowCount,
39611
+ effectiveSwarmOnSidebar
39612
+ });
39370
39613
  return;
39371
39614
  }
39372
39615
  if (key.downArrow) {
39373
- dispatch({ type: "sidebarScroll", delta: 1, viewportHeight: sidebarViewportHeight });
39616
+ dispatch({
39617
+ type: "sidebarScroll",
39618
+ delta: 1,
39619
+ viewportHeight: sidebarViewportHeight,
39620
+ sidebarTwinRowCount,
39621
+ effectiveSwarmOnSidebar
39622
+ });
39374
39623
  return;
39375
39624
  }
39376
39625
  if (key.escape) {
@@ -39419,7 +39668,9 @@ function createAppKeyHandler(options) {
39419
39668
  dispatch({
39420
39669
  type: "sidebarScroll",
39421
39670
  delta: key.mouse.wheel > 0 ? -1 : 1,
39422
- viewportHeight: termRows - 2
39671
+ viewportHeight: termRows - 2,
39672
+ sidebarTwinRowCount,
39673
+ effectiveSwarmOnSidebar
39423
39674
  });
39424
39675
  return;
39425
39676
  }
@@ -39864,11 +40115,33 @@ function App(props) {
39864
40115
  } = useLiveSettingsState({ getSettings, titleController, chime, confirmExit });
39865
40116
  const mailbox = useMailboxViewModel(events);
39866
40117
  const { setMailboxPanelOpen } = mailbox;
40118
+ const sidebarOpenFlags = {
40119
+ projectPicker: state.projectPicker.open,
40120
+ fleet: state.monitorOpen,
40121
+ agents: state.agentsMonitorOpen && (liveSettings?.showAgentSwarmPanel ?? "bottom") !== "off",
40122
+ worktree: state.worktreeMonitorOpen,
40123
+ plan: state.planPanelOpen,
40124
+ todos: state.todosMonitorOpen,
40125
+ queue: state.queuePanelOpen,
40126
+ processList: state.processListOpen,
40127
+ goal: state.goalPanelOpen,
40128
+ sessions: state.sessionsPanelOpen,
40129
+ coordinator: state.coordinator.monitorOpen || (state.goalRun?.monitorOpen ?? false),
40130
+ kanban: state.kanbanPanelOpen,
40131
+ connections: state.connectionsPanelOpen
40132
+ };
39867
40133
  const sidebarLayout = resolveSidebarLayout(
39868
40134
  state,
39869
40135
  stdout?.columns ?? 80,
39870
40136
  liveSettings?.panelPositions,
39871
- mailbox.mailboxPanelOpen
40137
+ mailbox.mailboxPanelOpen,
40138
+ sidebarOpenFlags,
40139
+ // Legacy `showAgentSwarmPanel === 'sidebar'` source from the
40140
+ // persisted config. Mirrors the renderer's read at
40141
+ // `app-view.tsx:897-899` so the scroll-clamp reservation matches
40142
+ // the actual source the renderer uses to decide whether to
40143
+ // render the mission card on the sidebar.
40144
+ (liveSettings?.showAgentSwarmPanel ?? "bottom") === "sidebar"
39872
40145
  );
39873
40146
  useEffect70(() => {
39874
40147
  titleController?.setModel(liveModel);
@@ -40343,6 +40616,8 @@ function App(props) {
40343
40616
  terminalColumns: stdout?.columns ?? 80,
40344
40617
  terminalRows: stdout?.rows ?? 24,
40345
40618
  mainColumnWidth: sidebarLayout.mainColumnWidth,
40619
+ effectiveSwarmOnSidebar: sidebarLayout.effectiveSwarmOnSidebar,
40620
+ sidebarTwinRowCount: sidebarLayout.sidebarTwinRowCount,
40346
40621
  statusBarWrapRef,
40347
40622
  belowStatusBarRef,
40348
40623
  liveStatuslineMode,
@@ -12,6 +12,8 @@ export type TokenSavingTierTui = 'auto' | 'off' | 'minimal' | 'light' | 'medium'
12
12
  export type FleetChatVerbosityTui = 'off' | 'full';
13
13
  export type ConfigScope = 'global' | 'project';
14
14
  export type AnimationStyleChoice = 'rainbow' | 'wave' | 'pulse' | 'dots' | 'breathe' | 'cycle';
15
+ /** Where to render the agent swarm panel: lower region, sidebar, or hidden. */
16
+ export type AgentSwarmPanelMode = 'bottom' | 'sidebar' | 'off';
15
17
  export type SettingsPickerPatch = Partial<{
16
18
  mode: SettingsMode;
17
19
  delayMs: number;
@@ -54,7 +56,7 @@ export type SettingsPickerPatch = Partial<{
54
56
  breakerEnabled: boolean;
55
57
  breakerAutoKillResetMs: number;
56
58
  showModelReasoning: boolean;
57
- showAgentSwarmPanel: import('./app-settings-type.js').AgentSwarmPanelMode;
59
+ showAgentSwarmPanel: AgentSwarmPanelMode;
58
60
  /**
59
61
  * Per-panel position map (one entry per F-key panel id). Each value is
60
62
  * 'bottom' (F-key behavior) or 'sidebar' (right-sidebar twin). Callers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tui",
3
- "version": "0.298.3",
3
+ "version": "0.299.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack terminal UI: Ink-based interactive renderer with history, pickers, and slash menus.",
6
6
  "repository": {
@@ -28,12 +28,12 @@
28
28
  "dependencies": {
29
29
  "ink": "7.1.1",
30
30
  "react": "^19.2.8",
31
- "@wrongstack/core": "0.298.3",
32
- "@wrongstack/tools": "0.298.3",
33
- "@wrongstack/kanban": "0.298.3",
34
- "@wrongstack/runtime": "0.298.3",
35
- "@wrongstack/sdd": "0.298.3",
36
- "@wrongstack/sage": "0.298.3"
31
+ "@wrongstack/runtime": "0.299.0",
32
+ "@wrongstack/kanban": "0.299.0",
33
+ "@wrongstack/core": "0.299.0",
34
+ "@wrongstack/sage": "0.299.0",
35
+ "@wrongstack/sdd": "0.299.0",
36
+ "@wrongstack/tools": "0.299.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@testing-library/dom": "^10.4.1",