@wrongstack/tui 0.298.0 → 0.298.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 (36) hide show
  1. package/dist/app-action-type.d.ts +16 -1
  2. package/dist/app-key-handler.d.ts +4 -0
  3. package/dist/app-settings-type.d.ts +30 -2
  4. package/dist/app-state.d.ts +17 -4
  5. package/dist/app-status-region.d.ts +7 -2
  6. package/dist/app-ui-state.d.ts +24 -0
  7. package/dist/auth-panel-model.d.ts +36 -2
  8. package/dist/components/fleet-panel.d.ts +3 -1
  9. package/dist/components/history/copy-geometry.d.ts +61 -0
  10. package/dist/components/history/sage-output-format.d.ts +7 -6
  11. package/dist/components/history/scroll-controller-types.d.ts +116 -0
  12. package/dist/components/history/scrollbar-geometry.d.ts +60 -0
  13. package/dist/components/history/scrollbar-rail.d.ts +18 -0
  14. package/dist/components/history/selection-helpers.d.ts +111 -0
  15. package/dist/components/input.d.ts +8 -1
  16. package/dist/components/plan-panel.d.ts +30 -3
  17. package/dist/components/scrollable-history.d.ts +7 -335
  18. package/dist/components/settings-picker-model.d.ts +13 -52
  19. package/dist/components/settings-picker.d.ts +10 -5
  20. package/dist/components/sidebar-content.d.ts +47 -0
  21. package/dist/components/sidebar-panel-frame.d.ts +80 -0
  22. package/dist/components/sidebar-panels.d.ts +136 -0
  23. package/dist/components/sidebar.d.ts +49 -0
  24. package/dist/components/status-bar-format.d.ts +10 -0
  25. package/dist/components/status-bar-types.d.ts +7 -0
  26. package/dist/components/status-bar.d.ts +1 -1
  27. package/dist/hit-test.d.ts +3 -2
  28. package/dist/hooks/use-provider-event-bridge.d.ts +1 -1
  29. package/dist/hooks/use-sidebar-panel-data.d.ts +59 -0
  30. package/dist/index.js +4224 -2010
  31. package/dist/input-validation/action-types.d.ts +1 -1
  32. package/dist/reducers/helpers.d.ts +1 -1
  33. package/dist/reducers/workspace-panels.d.ts +1 -1
  34. package/dist/settings-contracts.d.ts +11 -1
  35. package/dist/ui-contracts.d.ts +54 -0
  36. package/package.json +8 -8
@@ -279,7 +279,8 @@ export type Action = {
279
279
  breakerEnabled: boolean;
280
280
  breakerAutoKillResetMs: number;
281
281
  showModelReasoning: boolean;
282
- showAgentSwarmPanel: boolean;
282
+ showAgentSwarmPanel: import('./app-settings-type.js').AgentSwarmPanelMode;
283
+ panelPositions: import('./ui-contracts.js').PanelPositionMap;
283
284
  readSymbols: boolean;
284
285
  showSageMemoryInject: boolean;
285
286
  sageMemoryInjectThreshold: number;
@@ -929,6 +930,20 @@ export type Action = {
929
930
  /** Close every open panel/monitor/overlay at once. */
930
931
  | {
931
932
  type: 'closeAllPanels';
933
+ }
934
+ /** Toggle keyboard focus between input and the right sidebar. */
935
+ | {
936
+ type: 'toggleSidebarFocus';
937
+ }
938
+ /** Scroll the sidebar content by delta rows (clamped to >=0). */
939
+ | {
940
+ type: 'sidebarScroll';
941
+ delta: number;
942
+ viewportHeight?: number | undefined;
943
+ }
944
+ /** Reset sidebar scroll offset to 0. */
945
+ | {
946
+ type: 'sidebarScrollReset';
932
947
  } | {
933
948
  type: 'toggleKanbanPanel';
934
949
  } | {
@@ -47,6 +47,10 @@ interface AppKeyHandlerOptions {
47
47
  termRows: number;
48
48
  terminalColumns: number;
49
49
  terminalRows: number;
50
+ /** Width of the main column (terminal width minus sidebar). When > 0,
51
+ * scrollbar hit-tests use this instead of terminalColumns so the scrollbar
52
+ * track is correctly positioned to the left of the sidebar. */
53
+ mainColumnWidth: number;
50
54
  statusBarWrapRef: MutableRefObject<DOMElement | null>;
51
55
  belowStatusBarRef: MutableRefObject<DOMElement | null>;
52
56
  liveStatuslineMode: string;
@@ -1,5 +1,22 @@
1
1
  import type { FleetChatVerbosity, TokenSavingTier } from '@wrongstack/core/types';
2
2
  import type { CacheTtl, ContextMode, ReasoningEffort, StatuslineMode } from './settings-contracts.js';
3
+ export type { PanelId, PanelPosition, PanelPositionMap } from './ui-contracts.js';
4
+ export { coercePanelPositionMap, DEFAULT_PANEL_POSITIONS, PANEL_IDS, } from './ui-contracts.js';
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';
8
+ /**
9
+ * Coerce a persisted showAgentSwarmPanel value (which may be a legacy boolean
10
+ * or the new tri-state string) into a valid AgentSwarmPanelMode.
11
+ * - true / undefined → 'bottom' (default)
12
+ * - false → 'off'
13
+ * - Valid string ('bottom' | 'sidebar' | 'off') → returned as-is
14
+ * - Any other string → 'bottom' (safe fallback)
15
+ *
16
+ * This is the single canonical coercion — all read sites should call this
17
+ * rather than re-implementing the mapping to avoid divergent defaults.
18
+ */
19
+ export declare function coerceAgentSwarmMode(v: boolean | AgentSwarmPanelMode | string | undefined): AgentSwarmPanelMode;
3
20
  export type Settings = {
4
21
  mode: 'off' | 'suggest' | 'auto';
5
22
  delayMs: number;
@@ -56,8 +73,19 @@ export type Settings = {
56
73
  /** Single word shown in the TUI rainbow working-state chip. */
57
74
  thinkingWord: string;
58
75
  showModelReasoning: boolean;
59
- /** Show the persistent AGENT SWARM and todo mission queue panel. Default: true. */
60
- showAgentSwarmPanel: boolean;
76
+ /** Agent swarm panel placement: 'bottom' (lower region), 'sidebar' (right
77
+ * sidebar, vertical), or 'off' (hidden). Default: 'bottom'.
78
+ * Backward-compat: old boolean configs are coerced — true→'bottom', false→'off'. */
79
+ showAgentSwarmPanel: AgentSwarmPanelMode;
80
+ /**
81
+ * Per-panel position map. Each F-key panel can be set to 'bottom' (F-key
82
+ * behavior, the default) or 'sidebar' (rendered in the right sidebar).
83
+ * The map is always normalized to a full PanelPositionMap at boot via
84
+ * {@link coercePanelPositionMap}. Optional at the persisted-Settings
85
+ * boundary so older configs without the key don't typecheck-fail; the
86
+ * live-settings coercion always fills in a full map.
87
+ */
88
+ panelPositions?: Partial<PanelPositionMap>;
61
89
  /** Show SAGE Memory Inject blocks in tool results. Default: false (hidden). */
62
90
  showSageMemoryInject: boolean;
63
91
  /** Minimum relation strength for SAGE memory injection. Default: 0.85. */
@@ -7,7 +7,7 @@ import type { BrainLogEntry, BrainRiskLevel } from './brain-contracts.js';
7
7
  import type { BrainPanelSettings } from './brain-panel-model.js';
8
8
  import type { HistoryEntry } from './history-entry.js';
9
9
  import type { AuditLevel, CacheTtl, CompactorStrategy, ContextMode, LogLevel, ReasoningEffort, SettingsMode, StatuslineMode } from './settings-contracts.js';
10
- import type { AutonomyOption, ChipMeta, HelpEntry, LiveSessionEntry, McpPickerItem, ModeOption, PluginPickerItem, ProjectPickerItem, PromptPickEntry, ProviderOption, RefineFailureDecision, RefineFailureModel, SendMode, ShadowState, StatuslineItem, ToolPickerItem, WorktreeRow } from './ui-contracts.js';
10
+ import type { AutonomyOption, ChipMeta, HelpEntry, LiveSessionEntry, McpPickerItem, ModeOption, PanelPositionMap, PluginPickerItem, ProjectPickerItem, PromptPickEntry, ProviderOption, RefineFailureDecision, RefineFailureModel, SendMode, ShadowState, StatuslineItem, ToolPickerItem, WorktreeRow } from './ui-contracts.js';
11
11
  export type { Settings } from './app-settings-type.js';
12
12
  export type { DraftEntry, GoalSummary, QueueItem, ResumeSessionEntry, SlashCommandMatch, } from './app-state-core-types.js';
13
13
  export type { FleetEntry } from './app-state-fleet.js';
@@ -273,9 +273,18 @@ export type State = {
273
273
  * (API-level provisioning). Default: true.
274
274
  */
275
275
  showModelReasoning: boolean;
276
- /** Show the persistent AGENT SWARM and todo mission queue panel. Default: true. */
277
- showAgentSwarmPanel: boolean;
278
- /** Show SAGE Memory Inject blocks in tool results. Default: false. */
276
+ /** Agent swarm panel placement: 'bottom' (lower region), 'sidebar' (right sidebar), or 'off'. Default: 'bottom'. */
277
+ showAgentSwarmPanel: import('./app-settings-type.js').AgentSwarmPanelMode;
278
+ /**
279
+ * Per-panel position map mirrored from the persisted Settings shape.
280
+ * Each F-key panel can be set to 'bottom' (F-key behavior, the default)
281
+ * or 'sidebar' (rendered in the right sidebar). When a panel is set to
282
+ * 'sidebar' AND its F-key toggle is on, the bottom view hides and the
283
+ * sidebar shows the panel twin instead. When the F-key is off, neither
284
+ * surface shows the panel.
285
+ */
286
+ panelPositions: PanelPositionMap;
287
+ /** Show SAGE Memory Inject blocks in tool results. Default: false (hidden). */
279
288
  showSageMemoryInject: boolean;
280
289
  /** Minimum relation strength for SAGE memory injection. Default: 0.85. */
281
290
  sageMemoryInjectThreshold: number;
@@ -616,6 +625,10 @@ export type State = {
616
625
  connectionsPanelOpen: boolean;
617
626
  /** When true, the sessions panel is shown (F10). */
618
627
  sessionsPanelOpen: boolean;
628
+ /** When true, keyboard focus is on the right sidebar (↑↓ scrolls sidebar content). */
629
+ sidebarFocused: boolean;
630
+ /** Vertical scroll offset for the sidebar content (in rows). */
631
+ sidebarScrollOffset: number;
619
632
  /** Live session data for the sessions panel (F10). */
620
633
  sessionsPanel: {
621
634
  sessions: LiveSessionEntry[];
@@ -1,5 +1,10 @@
1
1
  import type React from 'react';
2
+ import type { AgentSwarmPanelMode } from './app-settings-type.js';
2
3
  import type { AppViewProps } from './app-view-contract.js';
3
- export declare function resolveAgentSwarmPanelVisibility(settingsOpen: boolean, pickerValue: boolean, persistedValue: boolean | undefined): boolean;
4
- export declare function AppStatusRegion({ host, runtime }: AppViewProps): React.ReactElement;
4
+ export declare function resolveAgentSwarmPanelVisibility(settingsOpen: boolean, pickerValue: AgentSwarmPanelMode, persistedValue: AgentSwarmPanelMode | undefined): AgentSwarmPanelMode;
5
+ export interface AppStatusRegionProps extends AppViewProps {
6
+ /** Optional column width cap for the status bar (when beside a sidebar). */
7
+ mainColumnWidth?: number | undefined;
8
+ }
9
+ export declare function AppStatusRegion({ host, runtime, mainColumnWidth }: AppStatusRegionProps): React.ReactElement;
5
10
  //# sourceMappingURL=app-status-region.d.ts.map
@@ -1,5 +1,29 @@
1
1
  import type { State } from './app-reducer.js';
2
2
  import type { StatuslineItem } from './components/statusline-picker.js';
3
+ import { type PanelId, type PanelPositionMap } from './ui-contracts.js';
3
4
  export declare function mergeStatuslineHiddenItems(hookHidden: StatuslineItem[], reducerHidden: StatuslineItem[]): StatuslineItem[];
5
+ /**
6
+ * True when any of the legacy picker overlays is open. Note: panel flags
7
+ * like `state.sessionsPanelOpen` are intentionally NOT included here so
8
+ * the local `overlayOpen` gate in app-view.tsx can apply its per-panel
9
+ * routing guard (`!anyRoutedToSidebar(id)`) and let the sidebar twin
10
+ * render. Adding a new panel-open flag to this list would silently
11
+ * collapse the sidebar to width 0 and prevent the twin from mounting.
12
+ */
4
13
  export declare function isPickerOverlayOpen(state: State): boolean;
14
+ export interface SidebarLayoutState {
15
+ panelPositions: PanelPositionMap;
16
+ overlayOpen: boolean;
17
+ sidebarWidth: number;
18
+ /** Width AppView must pass to nested routed sidebar panel frames. */
19
+ sidebarContentWidth: number;
20
+ mainColumnWidth: number;
21
+ }
22
+ /**
23
+ * Resolve the TUI's right-sidebar layout once from the open-panel flags and
24
+ * persisted panel routing. Both the renderer and input/mouse hit-testing use
25
+ * this so a sidebar-routed panel narrows history consistently instead of
26
+ * being treated as a full-width overlay by one surface.
27
+ */
28
+ export declare function resolveSidebarLayout(state: State, termCols: number, panelPositionsInput: Partial<Record<PanelId, 'bottom' | 'sidebar'>> | undefined, mailboxPanelOpen: boolean): SidebarLayoutState;
5
29
  //# sourceMappingURL=app-ui-state.d.ts.map
@@ -68,12 +68,37 @@ export interface AuthPanelHost {
68
68
  addKey(providerId: string, io: AuthFlowIo): Promise<AuthFlowResult>;
69
69
  updateKey(providerId: string, label: string, io: AuthFlowIo): Promise<AuthFlowResult>;
70
70
  editField(providerId: string, field: 'family' | 'baseUrl' | 'models', io: AuthFlowIo): Promise<AuthFlowResult>;
71
+ /**
72
+ * Edit a single model's detailed schema fields (ME-5). Opens a field-group
73
+ * prompt flow covering identity, limits, pricing, modalities, capability
74
+ * flags, and dates. Validates via the ME-1 zod schema.
75
+ *
76
+ * Shows the models.dev catalog reference values alongside the current
77
+ * values so the user can see what the upstream defaults are and reset
78
+ * individual fields back to them.
79
+ */
80
+ editModelDetails(providerId: string, modelId: string, io: AuthFlowIo): Promise<AuthFlowResult>;
81
+ /**
82
+ * Add a model to the provider's allowlist with optional full schema details
83
+ * (ME-5). When `fromCatalog` is true, prefills from the models.dev registry.
84
+ */
85
+ addModel(providerId: string, io: AuthFlowIo, opts?: {
86
+ fromCatalog?: boolean | undefined;
87
+ }): Promise<AuthFlowResult>;
88
+ /** Remove a single model from the provider's allowlist + customModels (ME-5). */
89
+ removeModel(providerId: string, modelId: string): Promise<string | null>;
90
+ /**
91
+ * Reset a model's customModels override back to the models.dev catalog
92
+ * values (ME-5). Drops the customModels entry for this modelId so the
93
+ * runtime falls back to the live catalog data.
94
+ */
95
+ resetModelToCatalog(providerId: string, modelId: string): Promise<string | null>;
71
96
  addCatalogProvider(catalogId: string, io: AuthFlowIo): Promise<AuthFlowResult>;
72
97
  addCustomProvider(io: AuthFlowIo): Promise<AuthFlowResult>;
73
98
  addLocal(presetId: string, io: AuthFlowIo): Promise<AuthFlowResult>;
74
99
  oauthLogin(kind: AuthOAuthKind, io: AuthFlowIo): Promise<AuthFlowResult>;
75
100
  }
76
- export type AuthPanelView = 'list' | 'provider' | 'catalog' | 'local' | 'oauth' | 'flow';
101
+ export type AuthPanelView = 'list' | 'provider' | 'models' | 'catalog' | 'local' | 'oauth' | 'flow';
77
102
  export type AuthConfirmAction = {
78
103
  kind: 'delete-key';
79
104
  providerId: string;
@@ -81,6 +106,10 @@ export type AuthConfirmAction = {
81
106
  } | {
82
107
  kind: 'remove-provider';
83
108
  providerId: string;
109
+ } | {
110
+ kind: 'remove-model';
111
+ providerId: string;
112
+ modelId: string;
84
113
  };
85
114
  export interface AuthPanelState {
86
115
  open: boolean;
@@ -125,7 +154,12 @@ export type AuthPanelRow = {
125
154
  keyRow: AuthKeyRow;
126
155
  } | {
127
156
  kind: 'provider-action';
128
- action: 'add-key' | 'edit-family' | 'edit-base-url' | 'edit-models' | 'remove';
157
+ action: 'add-key' | 'edit-family' | 'edit-base-url' | 'edit-models' | 'edit-model-details' | 'add-model' | 'reset-model-to-catalog' | 'remove' | 'back-to-list';
158
+ } | {
159
+ kind: 'model-row';
160
+ providerId: string;
161
+ modelId: string;
162
+ name: string;
129
163
  } | {
130
164
  kind: 'catalog-entry';
131
165
  entry: AuthCatalogRow;
@@ -21,6 +21,8 @@ export interface FleetPanelProps {
21
21
  planCount: number;
22
22
  evalCount: number;
23
23
  } | null;
24
+ /** Optional column width cap (when rendered beside a sidebar). */
25
+ maxWidth?: number | undefined;
24
26
  }
25
27
  type SwarmColor = 'blue' | 'cyan' | 'green' | 'gray' | 'red' | 'yellow';
26
28
  export interface SwarmCell {
@@ -69,6 +71,6 @@ export declare function buildTodoPreviewRows(todos: readonly TodoItem[] | undefi
69
71
  * monitor owns transcripts and drill-down; this panel stays compact and makes
70
72
  * the fleet's current shape readable at a glance.
71
73
  */
72
- export declare function FleetPanel({ entries, totalCost, roster, todos, nowTick, collabSession, }: FleetPanelProps): React.ReactElement | null;
74
+ export declare function FleetPanel({ entries, totalCost, roster, todos, nowTick, collabSession, maxWidth, }: FleetPanelProps): React.ReactElement | null;
73
75
  export {};
74
76
  //# sourceMappingURL=fleet-panel.d.ts.map
@@ -0,0 +1,61 @@
1
+ import type { HistoryEntry } from './index.js';
2
+ /**
3
+ * A clickable copy-icon target resolved in viewport coordinates during the
4
+ * post-render measurement pass. `entryId` identifies a single card or the first
5
+ * member of a compact tool group; `entryIds` contains every group member when
6
+ * present. `startRow`/`endRow` bound the box's visible rows (0-based from the
7
+ * viewport top, `endRow` exclusive); `iconCol` is the 0-based terminal column
8
+ * the icon renders at on the box's first visible row.
9
+ */
10
+ export interface CopyHit {
11
+ entryId: number;
12
+ entryIds?: readonly number[] | undefined;
13
+ startRow: number;
14
+ endRow: number;
15
+ iconCol: number;
16
+ }
17
+ /** Sentinel returned after copying the active, non-retained tool-stream box. */
18
+ export declare const LIVE_TOOL_STREAM_COPY_ID = -1;
19
+ /** Sentinel returned after copying a drag-select-then-right-click selection.
20
+ * Distinct from any retained entry id so the host can render a different
21
+ * status-line notice ("Selected range copied") without flashing a specific
22
+ * card's copy icon (which only makes sense when a single entry was copied). */
23
+ export declare const SELECTION_COPY_ID = -2;
24
+ /**
25
+ * Resolve the copy target under a viewport cell, or null. A cell matches when
26
+ * its `row` is within the card's visible rows and its `col` is within the
27
+ * icon's cell span. Iterates newest-first so overlapping row estimates favor
28
+ * the most recently rendered card. Exported-shape pure helper for unit tests.
29
+ */
30
+ export declare function findCopyHit(hits: readonly CopyHit[], row: number, col: number): CopyHit | null;
31
+ /** Resolve a hit to the complete current clipboard payload without performing I/O. */
32
+ export declare function resolveCopyPayload(hit: CopyHit, entriesById: ReadonlyMap<number, HistoryEntry>, liveToolText?: string | undefined): {
33
+ entryId: number;
34
+ text: string;
35
+ } | null;
36
+ /**
37
+ * Rows clipped from the top of the mounted history stack before it appears in
38
+ * the viewport. Scrolled frames clip by the anchor's row offset; pinned frames
39
+ * rely on Ink flex-end clipping, which hides top overflow from mounted groups
40
+ * plus the live tool tail.
41
+ */
42
+ export declare function copyRegistryVisibleClip(opts: {
43
+ scrolled: boolean;
44
+ clip: number;
45
+ mountedRows: number;
46
+ tailRows: number;
47
+ viewportRows: number;
48
+ }): number;
49
+ /** Build the live tool-stream header hit, accounting for ToolStreamBox's top margin.
50
+ * The `pinnedSlack` offset must match {@link buildCopyRegistry}'s card-hit math so
51
+ * the live-stream icon stays visually aligned with its header row in underfilled
52
+ * pinned frames (flex-end parks the mounted stack at the bottom of the viewport). */
53
+ export declare function liveToolStreamCopyHit(opts: {
54
+ visible: boolean;
55
+ mountedRows: number;
56
+ visibleClip: number;
57
+ viewportRows: number;
58
+ iconCol: number;
59
+ pinnedSlack?: number;
60
+ }): CopyHit | null;
61
+ //# sourceMappingURL=copy-geometry.d.ts.map
@@ -20,12 +20,13 @@ export interface ParsedSageMemoryLine {
20
20
  tags?: string[] | undefined;
21
21
  }
22
22
  /**
23
- * Strict counterpart to `SAGE_MEMORY_LINE`. The gate above only decides "does
24
- * this line belong to a SAGE block?" and stays permissive (greedy body, opaque
25
- * `(?: .*)?` trailer) so block extraction survives future format additions.
26
- * This one captures named groups and therefore requires the exact `(anchor)` /
27
- * `[meta]` shapes the producer emits today; a line that passes the gate but
28
- * fails here falls back to a literal render row, so the memory is still shown.
23
+ * Strict counterpart to the canonical gate regex (kept private in
24
+ * `sage-output-block.ts`). The gate only decides "does this line belong to a
25
+ * SAGE block?" and stays permissive (greedy body, opaque `(?: .*)?` trailer)
26
+ * so block extraction survives future format additions. This one captures
27
+ * named groups and therefore requires the exact `(anchor)` / `[meta]` shapes
28
+ * the producer emits today; a line that passes the gate but fails here falls
29
+ * back to a literal render row, so the memory is still shown.
29
30
  */
30
31
  export declare const SAGE_MEMORY_DETAIL: RegExp;
31
32
  export declare function parseSageMemoryLine(line: string): ParsedSageMemoryLine | null;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Public types for the scrollable history viewport.
3
+ *
4
+ * Extracted from scrollable-history.tsx so the imperative controller
5
+ * contract, props, and shared geometry types can be imported without
6
+ * pulling in the full component.
7
+ */
8
+ import type { MutableRefObject } from 'react';
9
+ import type { HistoryProps } from './index.js';
10
+ /**
11
+ * Imperative scroll surface owned by the ScrollableHistory component. The app
12
+ * key/mouse handlers call these instead of dispatching reducer actions: all
13
+ * scroll math needs the live height cache, which lives here, and routing it
14
+ * through app state previously forced totals and offsets to travel through
15
+ * separate commits — the root cause of an entire family of jump bugs.
16
+ */
17
+ export interface HistoryScrollController {
18
+ /** Scroll by `deltaUp` rows: positive = older content, negative = newer. */
19
+ scrollBy(deltaUp: number): void;
20
+ /** Scroll by one viewport page. */
21
+ scrollPage(dir: 'up' | 'down'): void;
22
+ /** Jump to the oldest retained entry. */
23
+ scrollToTop(): void;
24
+ /** Re-pin to the newest output. */
25
+ scrollToBottom(): void;
26
+ /** Jump to a 0-based cell clicked/dragged on the scrollbar track. */
27
+ scrollToTrackCell(cell: number): void;
28
+ /** True while the viewport is scrolled away from the newest output. */
29
+ isScrolled(): boolean;
30
+ /**
31
+ * True when the viewport cell lands on a copyable card's copy icon. Lets the
32
+ * mouse handler decide synchronously whether to consume the click before
33
+ * firing the async copy. See {@link copyAtViewportCell} for the `row`/`col`
34
+ * coordinate contract.
35
+ */
36
+ hasCopyTargetAt(row: number, col: number): boolean;
37
+ /**
38
+ * Handle a left-click inside the history viewport. `row` is 0-based from the
39
+ * viewport top; `col` is 0-based from the LEFT EDGE OF THE HISTORY BAND, not
40
+ * the raw terminal column. In the interactive mount the band renders flush at
41
+ * terminal column 0 (the scrollbar occupies the rightmost columns), so the
42
+ * caller passes the terminal column directly; a band that is inset from the
43
+ * left must subtract its left offset first. When the cell lands on a copyable
44
+ * card's copy icon, its content is written to the system clipboard and the
45
+ * entry id is returned; otherwise returns null.
46
+ */
47
+ copyAtViewportCell(row: number, col: number): Promise<number | null>;
48
+ /**
49
+ * Begin a drag-to-select gesture at `row`,`col` (history-band viewport cell).
50
+ * The cell must be inside a card's row range and inside the rendered band
51
+ * (`0 <= col < termWidth`); the mouse handler has already excluded the rail.
52
+ * Calls that don't satisfy that contract are silently ignored. Existing
53
+ * scrollbar/copy-icon/wheel paths remain intact because they call their own
54
+ * handlers; this only kicks in when the mouse layer explicitly routes a
55
+ * left-press into the card band.
56
+ */
57
+ beginSelection(row: number, col: number): void;
58
+ /**
59
+ * Extend an active selection to `row`,`col`. If no selection is in progress,
60
+ * the call is a no-op. Crossing into the gutter or below the viewport cancels
61
+ * the drag internally — leaving stale viewport-relative coords around after
62
+ * a mid-drag escape would let the next right-click paste from the wrong
63
+ * card, which is the worse failure mode than the selection disappearing.
64
+ */
65
+ extendSelection(row: number, col: number): void;
66
+ /**
67
+ * End an active drag (left-release). Does NOT write to the clipboard — the
68
+ * copy is performed by {@link commitSelection}, which is triggered by a
69
+ * separate right-click in the history band (per the drag-select-then-commit
70
+ * contract). If no selection is in progress, the call is a no-op.
71
+ */
72
+ endSelection(): void;
73
+ /** Drop any in-progress or committed selection without copying. */
74
+ clearSelection(): void;
75
+ /**
76
+ * Commit the active selection to the system clipboard. Returns true if a
77
+ * non-empty selection was resolved and the write succeeded. Returns false
78
+ * silently when there is no selection, the selection is empty (e.g. the drag
79
+ * never moved), or the write fails — the caller can show a transient notice
80
+ * on the truthy branch and stay silent on the falsy branch.
81
+ */
82
+ commitSelection(): Promise<boolean>;
83
+ }
84
+ export interface ScrollableHistoryProps extends HistoryProps {
85
+ /** Height of the viewport in rows, computed by App from the bottom region. */
86
+ viewportRows: number;
87
+ /** Receives the imperative scroll controller. The component assigns on
88
+ * mount and clears on unmount. Optional for isolated renderers. */
89
+ controllerRef?: MutableRefObject<HistoryScrollController | null> | undefined;
90
+ /** Reports scroll-state transitions (scrolled away from / re-pinned to the
91
+ * newest output) so the host can adjust key hints. */
92
+ onScrollInfo?: ((info: {
93
+ scrolled: boolean;
94
+ }) => void) | undefined;
95
+ /** Optional cap on the width used for entry wrapping (right panel mode). */
96
+ maxWidth?: number | undefined;
97
+ /** Layout store for persisting entry height data across renders and sessions.
98
+ * When provided, the ScrollableHistory seeds its height cache from the
99
+ * store's persisted measurements and marks entries as measured after render,
100
+ * eliminating estimate-vs-actual scroll jumps on re-render. */
101
+ layoutStore?: import('../../layout-store.js').LayoutStore | undefined;
102
+ /**
103
+ * Entry id of the card whose copy icon was just clicked. That card's icon
104
+ * renders in the success color for the brief window the host keeps this set,
105
+ * giving per-card visual feedback alongside the status-line "Copied" notice.
106
+ * `null` / undefined = no card highlighted.
107
+ */
108
+ copiedEntryId?: number | null | undefined;
109
+ /**
110
+ * Called when the user scrolls near the top of the currently loaded
111
+ * entries. The host should respond by loading older entries from the
112
+ * history archive and dispatching `archiveLoaded` to prepend them.
113
+ */
114
+ onRequestOlderEntries?: (() => void) | undefined;
115
+ }
116
+ //# sourceMappingURL=scroll-controller-types.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Scrollbar math and card-span geometry — pure functions for thumb position,
3
+ * track-cell-to-offset mapping, and mounted card viewport spans.
4
+ *
5
+ * Extracted from scrollable-history.tsx.
6
+ */
7
+ import type { EntryHeightCache } from '../../height-cache.js';
8
+ import { type RenderGroup } from './tool-group.js';
9
+ /** Pure thumb geometry for the scrollbar: where the thumb starts and how many
10
+ * cells it spans, given the track height, scroll offset, and total content
11
+ * height. Exported for testing. */
12
+ export declare function scrollbarThumb(rows: number, offset: number, total: number): {
13
+ top: number;
14
+ size: number;
15
+ scrollable: boolean;
16
+ };
17
+ /** Inverse of {@link scrollbarThumb}: given a clicked/dragged 0-based cell on a
18
+ * track of `rows` height, return the scroll offset (rows up from the bottom)
19
+ * that lands the visible window there. Cell 0 (top) → oldest content (max
20
+ * offset); cell rows-1 (bottom) → newest (offset 0). Exported for testing. */
21
+ export declare function scrollOffsetForTrackRow(rows: number, total: number, cell: number): number;
22
+ /**
23
+ * Per-card viewport span for the currently-mounted groups. Mirrors
24
+ * {@link buildCopyRegistry}'s visibleClip math so the drag-select gesture asks
25
+ * the same geometry question the copy-hit registry already answers.
26
+ */
27
+ export interface MountedCardSpan {
28
+ entryId: number;
29
+ /** 0-based viewport row of the first VISIBLE row of the card. */
30
+ viewportStartRow: number;
31
+ /** 0-based viewport row one past the last VISIBLE row of the card. */
32
+ viewportEndRow: number;
33
+ /** Card's geometry in rows; visibleClip + scrolled-clip aware. */
34
+ totalRows: number;
35
+ entryIds?: readonly number[];
36
+ }
37
+ /**
38
+ * Build per-card viewport spans for every mounted render group. Pure — given
39
+ * the same render-group list, height cache, scroll state, and viewport size,
40
+ * the result is deterministic and matches the copy-hit registry. Exported so
41
+ * tests can verify the span map without mounting the full component.
42
+ */
43
+ export declare function buildMountedCardSpans(opts: {
44
+ renderGroups: readonly RenderGroup[];
45
+ heightCache: EntryHeightCache;
46
+ scrolled: boolean;
47
+ clip: number;
48
+ tailRows: number;
49
+ viewportRows: number;
50
+ showModelReasoning?: boolean | undefined;
51
+ }): MountedCardSpan[];
52
+ /**
53
+ * Find the card whose viewport row range contains `row`. Pure.
54
+ * Returns null when the row is in a blank gap or outside the viewport.
55
+ */
56
+ export declare function selectionHitAt(row: number, spans: readonly MountedCardSpan[]): {
57
+ entryId: number;
58
+ entryIds?: readonly number[];
59
+ } | null;
60
+ //# sourceMappingURL=scrollbar-geometry.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Right-edge rail for the managed viewport. Its first column is the copy icon
3
+ * (or blank), the second is a visual gap, and the third is the scrollbar track.
4
+ * All three columns are always reserved, so copy affordances and scrollability
5
+ * never reflow chat content.
6
+ *
7
+ * Extracted from scrollable-history.tsx.
8
+ */
9
+ import type React from 'react';
10
+ import type { CopyHit } from './copy-geometry.js';
11
+ export declare function Scrollbar({ rows, offset, total, copyHits, copiedEntryId, }: {
12
+ rows: number;
13
+ offset: number;
14
+ total: number;
15
+ copyHits: readonly CopyHit[];
16
+ copiedEntryId?: number | null | undefined;
17
+ }): React.ReactElement;
18
+ //# sourceMappingURL=scrollbar-rail.d.ts.map
@@ -0,0 +1,111 @@
1
+ import type { HistoryEntry } from './index.js';
2
+ /**
3
+ * Selection rectangle in viewport cell coordinates, normalized so
4
+ * `topLeft.row <= bottomRight.row` and the column pairs are likewise ordered.
5
+ * Both endpoints are inclusive — the cell at `bottomRight` is selected.
6
+ */
7
+ export interface SelectionRect {
8
+ topLeft: {
9
+ row: number;
10
+ col: number;
11
+ };
12
+ bottomRight: {
13
+ row: number;
14
+ col: number;
15
+ };
16
+ /** True while the user is still dragging; false after left-release but
17
+ * before either clearSelection or commitSelection runs. */
18
+ inProgress: boolean;
19
+ }
20
+ /** Canonicalize two viewport cells into a SelectionRect. Pure. */
21
+ export declare function normalizeSelection(a: {
22
+ row: number;
23
+ col: number;
24
+ }, b: {
25
+ row: number;
26
+ col: number;
27
+ }, inProgress: boolean): SelectionRect;
28
+ /** True when the column lies outside the rendered card band. Inside the band
29
+ * the layout reserves `SCROLLBAR_HIT_WIDTH` columns for the rail AFTER the
30
+ * band, so `0..termWidth-1` is always in-band and nothing inside needs to be
31
+ * excluded. The mouse handler must already have excluded the rail before
32
+ * calling into the controller — we only check the band bounds here. */
33
+ export declare function isOutOfBand(col: number, termWidth: number): boolean;
34
+ /**
35
+ * One entry's contribution to a drag-selected range. Returned in document
36
+ * (row-index-in-card) coordinates so the caller decides how to slice into
37
+ * the entry's source text. `start` is the column inside the card (0-based
38
+ * from the left edge of the card's visible rectangle, NOT the terminal
39
+ * column — the gutter is excluded by the caller before reaching here).
40
+ * `end` is inclusive. When `startRow === endRow`, the slice is one row; the
41
+ * caller is responsible for translating text columns into source offsets.
42
+ */
43
+ export interface SelectionSlice {
44
+ entryId: number;
45
+ /** Inclusive row-in-card for the start of the slice. */
46
+ startRow: number;
47
+ /** Inclusive column, 0-based, inside the card's text area (gutter excluded). */
48
+ startCol: number;
49
+ /** Inclusive end row. */
50
+ endRow: number;
51
+ /** Inclusive end column, 0-based. */
52
+ endCol: number;
53
+ }
54
+ /**
55
+ * Translate a viewport-cell selection into per-entry row/col slices, walking
56
+ * the same mounted-group geometry the copy-hit registry uses so the two
57
+ * systems share one row index. Entries wholly outside the selection return
58
+ * nothing. Entries whose vertical span is wholly inside the selection are
59
+ * returned as a single row-range; entries the selection clips at the top or
60
+ * bottom are returned with row bounds matching the clip.
61
+ *
62
+ * `cardVisibleCols` is the number of columns inside the card the user can
63
+ * see — the band width minus the right gutter (`viewportWidth - SCROLLBAR_HIT_WIDTH`).
64
+ * Slices never include the gutter or any column to its right; the caller has
65
+ * already validated that.
66
+ *
67
+ * Returned slices are in document (entry-local) coordinates: `startRow`/`endRow`
68
+ * are rows inside the entry's own geometry, NOT viewport cells. This is what
69
+ * the assembler needs to recover text — slicing into the raw entry text uses
70
+ * per-row layout, which the caller resolves via the layout store.
71
+ */
72
+ export declare function selectionToSlices(opts: {
73
+ selection: SelectionRect;
74
+ cards: ReadonlyArray<{
75
+ entryId: number;
76
+ /** 0-based viewport row of the card's first visible row. */
77
+ viewportStartRow: number;
78
+ /** 0-based viewport row just past the card's last visible row. */
79
+ viewportEndRow: number;
80
+ }>;
81
+ cardVisibleCols: number;
82
+ }): SelectionSlice[];
83
+ /**
84
+ * Build the clipboard payload for a selection by slicing into each affected
85
+ * entry's full renderable text. The v1 contract is line-then-column on the
86
+ * entry's source text: each slice exposes `startRow`/`endRow` and
87
+ * `startCol`/`endCol` in card-local coordinates; we split the entry's
88
+ * text on `\n`, keep only the rows inside the row range, and within those
89
+ * rows apply the column range. The matrix matches what the source line
90
+ * index would be when no wrapping occurs — i.e. one card row maps to one
91
+ * source line. Wrap-geometry translation is intentionally absent in v1
92
+ * because the layout store does not yet expose per-card-row wrap segments;
93
+ * it returns when those segments exist and a v1.1 test demands it.
94
+ *
95
+ * `toolGroupsByHeadId` lets a multi-member tool-group expand: when the
96
+ * head id appears with `entryIds.length > 1`, the assembler copies via
97
+ * `copyableTextForEntries` (raw ordered JSON of every member, matching
98
+ * the existing copy-icon contract). A single-entry head falls through
99
+ * to `copyableTextForEntry`.
100
+ *
101
+ * Multi-entry selections are joined with `\n---\n` so the user can see
102
+ * where one card ends and the next begins. Empty selections and
103
+ * selections that resolve to no text return `""` so the caller can fall
104
+ * through silently.
105
+ */
106
+ export declare function assembleSelectionText(opts: {
107
+ slices: readonly SelectionSlice[];
108
+ entriesById: ReadonlyMap<number, HistoryEntry>;
109
+ toolGroupsByHeadId?: ReadonlyMap<number, readonly number[]> | undefined;
110
+ }): string;
111
+ //# sourceMappingURL=selection-helpers.d.ts.map