@aragon-agent/cli 0.6.13 → 0.6.15

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 (131) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +250 -222
  3. package/dist/agent/controller.d.ts +36 -2
  4. package/dist/agent/controller.js +244 -57
  5. package/dist/agent/model-profile-settings.js +11 -2
  6. package/dist/agent/plan-prompt.d.ts +1 -1
  7. package/dist/agent/plan-prompt.js +4 -3
  8. package/dist/agent/project-guidance-prompt.d.ts +4 -0
  9. package/dist/agent/project-guidance-prompt.js +104 -0
  10. package/dist/agent/reducer.d.ts +2 -0
  11. package/dist/agent/reducer.js +5 -0
  12. package/dist/agent/system-prompt.js +4 -0
  13. package/dist/boot/guard.d.ts +3 -3
  14. package/dist/boot/guard.js +3 -3
  15. package/dist/cli.js +33 -15
  16. package/dist/commands/builtins.js +2 -2
  17. package/dist/compaction/child.d.ts +9 -1
  18. package/dist/compaction/child.js +43 -13
  19. package/dist/compaction/command.js +20 -10
  20. package/dist/compaction/compactor.d.ts +33 -204
  21. package/dist/compaction/compactor.js +311 -723
  22. package/dist/compaction/context-command.js +5 -3
  23. package/dist/compaction/digest.d.ts +15 -51
  24. package/dist/compaction/digest.js +113 -148
  25. package/dist/compaction/limits.d.ts +19 -142
  26. package/dist/compaction/limits.js +20 -147
  27. package/dist/compaction/memory-identity.d.ts +16 -0
  28. package/dist/compaction/memory-identity.js +63 -0
  29. package/dist/compaction/memory-input.d.ts +25 -0
  30. package/dist/compaction/memory-input.js +106 -0
  31. package/dist/compaction/memory-render.d.ts +4 -0
  32. package/dist/compaction/memory-render.js +47 -0
  33. package/dist/compaction/memory-validation.d.ts +21 -0
  34. package/dist/compaction/memory-validation.js +268 -0
  35. package/dist/compaction/memory.d.ts +79 -0
  36. package/dist/compaction/memory.js +118 -0
  37. package/dist/compaction/meter.d.ts +14 -10
  38. package/dist/compaction/meter.js +55 -19
  39. package/dist/compaction/operation.d.ts +18 -0
  40. package/dist/compaction/operation.js +68 -0
  41. package/dist/compaction/pressure.d.ts +6 -20
  42. package/dist/compaction/pressure.js +14 -23
  43. package/dist/compaction/summarize-call.d.ts +3 -16
  44. package/dist/compaction/summarize-call.js +20 -20
  45. package/dist/compaction/summary-prompt.d.ts +29 -66
  46. package/dist/compaction/summary-prompt.js +132 -178
  47. package/dist/compaction/types.d.ts +23 -3
  48. package/dist/compaction/wiring.d.ts +10 -25
  49. package/dist/compaction/wiring.js +60 -67
  50. package/dist/config/load.d.ts +3 -1
  51. package/dist/config/load.js +8 -4
  52. package/dist/config/schema.d.ts +3 -3
  53. package/dist/config/schema.js +1 -1
  54. package/dist/exec/events.d.ts +8 -0
  55. package/dist/exec/index.js +13 -6
  56. package/dist/exec/runner.d.ts +8 -0
  57. package/dist/exec/runner.js +26 -2
  58. package/dist/session/persist.d.ts +3 -0
  59. package/dist/session/persist.js +9 -0
  60. package/dist/session/store.js +2 -0
  61. package/dist/session/validate-session.js +34 -0
  62. package/dist/team/subagent.js +8 -0
  63. package/dist/ui/App.js +221 -126
  64. package/dist/ui/BottomStatusRow.d.ts +5 -10
  65. package/dist/ui/BottomStatusRow.js +7 -11
  66. package/dist/ui/Composer.d.ts +4 -2
  67. package/dist/ui/Composer.js +2 -2
  68. package/dist/ui/Header.d.ts +6 -2
  69. package/dist/ui/Header.js +7 -6
  70. package/dist/ui/Logo.d.ts +3 -5
  71. package/dist/ui/Logo.js +4 -8
  72. package/dist/ui/PromptInput.d.ts +3 -1
  73. package/dist/ui/PromptInput.js +35 -42
  74. package/dist/ui/StatusBar.d.ts +6 -0
  75. package/dist/ui/StatusBar.js +8 -7
  76. package/dist/ui/Transcript.js +1 -1
  77. package/dist/ui/capabilities.d.ts +2 -0
  78. package/dist/ui/capabilities.js +6 -0
  79. package/dist/ui/color-runtime.d.ts +5 -0
  80. package/dist/ui/color-runtime.js +29 -0
  81. package/dist/ui/entries/CompactionCard.d.ts +3 -1
  82. package/dist/ui/entries/CompactionCard.js +7 -5
  83. package/dist/ui/frame-differ.js +27 -0
  84. package/dist/ui/frame-observer.js +18 -5
  85. package/dist/ui/glyphs.js +10 -8
  86. package/dist/ui/ink-runtime.d.ts +3 -0
  87. package/dist/ui/ink-runtime.js +30 -0
  88. package/dist/ui/interaction-copy.d.ts +73 -73
  89. package/dist/ui/interaction-copy.js +30 -38
  90. package/dist/ui/interaction-hints.d.ts +8 -1
  91. package/dist/ui/interaction-hints.js +56 -69
  92. package/dist/ui/layout/AppShell.d.ts +6 -2
  93. package/dist/ui/layout/AppShell.js +2 -2
  94. package/dist/ui/layout/OverlayFrame.d.ts +2 -0
  95. package/dist/ui/layout/OverlayFrame.js +23 -14
  96. package/dist/ui/layout/ScrollViewport.d.ts +3 -1
  97. package/dist/ui/layout/ScrollViewport.js +10 -2
  98. package/dist/ui/layout/budget.d.ts +25 -26
  99. package/dist/ui/layout/budget.js +29 -22
  100. package/dist/ui/layout/document-layout.d.ts +2 -2
  101. package/dist/ui/layout/document-layout.js +2 -3
  102. package/dist/ui/layout/queue-layout.js +4 -4
  103. package/dist/ui/layout/status-detail-layout.d.ts +9 -0
  104. package/dist/ui/layout/status-detail-layout.js +56 -0
  105. package/dist/ui/layout/status-layout.d.ts +19 -2
  106. package/dist/ui/layout/status-layout.js +95 -83
  107. package/dist/ui/layout/wrap-rows.d.ts +2 -0
  108. package/dist/ui/layout/wrap-rows.js +21 -7
  109. package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
  110. package/dist/ui/overlays/ConfirmDialog.js +22 -3
  111. package/dist/ui/overlays/HelpOverlay.js +9 -2
  112. package/dist/ui/overlays/ModelPicker.d.ts +5 -8
  113. package/dist/ui/overlays/ModelPicker.js +44 -10
  114. package/dist/ui/overlays/ModelProfileEditor.js +2 -0
  115. package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
  116. package/dist/ui/overlays/ModelProfilePicker.js +4 -2
  117. package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
  118. package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
  119. package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
  120. package/dist/ui/overlays/QuestionOverlay.js +45 -32
  121. package/dist/ui/overlays/QueueOverlay.js +2 -2
  122. package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
  123. package/dist/ui/overlays/SettingsScreen.js +10 -3
  124. package/dist/ui/render-cache.d.ts +2 -2
  125. package/dist/ui/render-cache.js +4 -3
  126. package/dist/ui/run-metrics.d.ts +23 -0
  127. package/dist/ui/run-metrics.js +27 -0
  128. package/dist/ui/status-feedback.d.ts +33 -0
  129. package/dist/ui/status-feedback.js +36 -0
  130. package/dist/ui/stdout-frame-writer.js +28 -0
  131. package/package.json +2 -2
@@ -1,6 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Confirm dialog — Yes/No gate for `confirmTools` mode. Resolves the pending
4
+ * tool-execution promise (spec §3.6). `y` = approve; `n`/Enter/Esc = reject
5
+ * (Enter follows the safe default shown by the capital N in the "(y/N)" prompt,
6
+ * since this gates mutating tools like write_file/edit_file/bash).
7
+ */
8
+ import { useState } from 'react';
2
9
  import { Text, useInput } from 'ink';
3
10
  import { pickGlyphs } from '../glyphs.js';
11
+ import { wrapToRows } from '../layout/wrap-rows.js';
4
12
  import { OverlayFrame } from '../layout/OverlayFrame.js';
5
13
  /**
6
14
  * Self-managed (mode B) and deliberately without a position indicator: five
@@ -9,9 +17,20 @@ import { OverlayFrame } from '../layout/OverlayFrame.js';
9
17
  * title and border match the other three overlays; the existing `useInput` is
10
18
  * untouched.
11
19
  */
12
- export function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }) {
20
+ export function ConfirmDialog({ state, isActive = true, maxRows, cols, theme, caps, onClose, }) {
13
21
  const glyphs = pickGlyphs(caps);
22
+ const [offset, setOffset] = useState(0);
14
23
  useInput((input, key) => {
24
+ if (!isActive)
25
+ return;
26
+ if (key.pageUp || key.upArrow) {
27
+ setOffset(n => Math.max(0, n - 1));
28
+ return;
29
+ }
30
+ if (key.pageDown || key.downArrow) {
31
+ setOffset(n => n + 1);
32
+ return;
33
+ }
15
34
  if (input === 'y' || input === 'Y') {
16
35
  state.resolve(true);
17
36
  onClose();
@@ -21,5 +40,5 @@ export function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }) {
21
40
  onClose();
22
41
  }
23
42
  });
24
- return (_jsxs(OverlayFrame, { title: "Confirm action", hint: `y approve ${glyphs.midDot} n / Enter / Esc reject`, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsx(Text, { color: theme.assistant, children: state.summary }), _jsx(Text, { color: theme.muted, children: "Proceed? (y/N)" })] }));
43
+ return (_jsx(OverlayFrame, { title: "Confirm action", hint: "y approve | n/Enter/Esc reject | PgDn", maxRows: maxRows, cols: cols, theme: theme, caps: caps, scrollOffset: offset, onScrollClamp: setOffset, rows: wrapToRows(state.summary, Math.max(1, cols - 4)).map((line, index) => _jsx(Text, { wrap: "truncate", color: theme.assistant, children: line || ' ' }, index)) }));
25
44
  }
@@ -20,6 +20,7 @@ function keyRows(times) {
20
20
  [`Ctrl+C ${times}2`, 'Exit'],
21
21
  ['Ctrl+L', 'Redraw the frame'],
22
22
  ['Ctrl+T', 'Show/hide thinking (off by default)'],
23
+ ['Ctrl+G', 'Expand/collapse status details; disabled while an overlay is open'],
23
24
  ['PgUp / PgDn', 'Scroll a page (transcript, or this overlay)'],
24
25
  ['Shift+Up / Shift+Down', 'Scroll the transcript a line (full-screen mode)'],
25
26
  ['Wheel', 'Scroll the transcript (or the open overlay)'],
@@ -27,8 +28,14 @@ function keyRows(times) {
27
28
  // Drag-select is the one affordance here a user cannot discover by pressing
28
29
  // a key, so it has to be named somewhere they can look it up. The one-shot
29
30
  // startup notice is the other place, and it scrolls away.
30
- ['Drag right edge', 'Scroll messages and input together; type to return'],
31
- ['Bottom rows', 'Input, fixed action hints, then global runtime / queue / context status'],
31
+ ['Drag right edge', 'Scroll messages; input stays fixed; type to return to newest output'],
32
+ ['Bottom rows', 'Fixed input, one status row; Ctrl+G adds one detail row'],
33
+ ['C / Th / t/s', 'Context percent / thinking level / run-average output tokens per second'],
34
+ ['O/N/L/M/H/X', 'Thinking: off/minimal/low/medium/high/xhigh'],
35
+ ['~ / ? / --', 'Estimated context / unknown context / unavailable output speed'],
36
+ ['Output speed', 'Main Agent reported output tokens / total run time, including tools and waits'],
37
+ ['Copy / Sent / E / F', 'Copying / unconfirmed copy sent / copy error / Esc force-stop'],
38
+ ['^N', 'Transcript line offset from the bottom, not a new-message count'],
32
39
  ['Drag text (left button)', 'Select text on screen; the highlight waits'],
33
40
  ['Ctrl+C (with a selection)', 'Copy the pending selection - does not arm exit'],
34
41
  ['Native terminal selection', 'Copy-on-select is controlled by your terminal settings'],
@@ -3,19 +3,16 @@
3
3
  * providers only (R5) so it never offers a model whose provider would fail at
4
4
  * run with `Unknown LLM provider`. Lists each provider's builtin models.
5
5
  *
6
- * Uses `OverlayFrame`'s SELF-MANAGED mode (§4.4 / R-12). `ink-select-input`
7
- * windows itself via `limit` and registers its own `useInput` with `isFocused`
8
- * defaulting to true; Ink delivers every key to every mounted handler, so a
9
- * frame that also claimed the arrow keys would move the selection twice per
10
- * press. Slicing it by element would be worse still — it is one component, not
11
- * a list of rows. So the frame draws chrome only, and `App` registers no arrow
12
- * keys while this overlay is open.
6
+ * Selection belongs to a model ID, independently of the visible row window.
7
+ * Resizing or hiding the overlay never resets it. Each item occupies one row;
8
+ * this component owns navigation while App owns Escape.
13
9
  */
14
10
  import React from 'react';
15
11
  import type { ModelRegistry } from '@aragon-agent/core';
16
12
  import type { Theme } from '../theme.js';
17
13
  import type { TermCapabilities } from '../capabilities.js';
18
14
  interface ModelPickerProps {
15
+ isActive?: boolean;
19
16
  registry: ModelRegistry;
20
17
  currentProvider: string;
21
18
  currentModel: string;
@@ -25,5 +22,5 @@ interface ModelPickerProps {
25
22
  caps: TermCapabilities;
26
23
  onSelect: (provider: string, model: string) => void;
27
24
  }
28
- export declare function ModelPicker({ registry, currentProvider, currentModel, maxRows, cols, theme, caps, onSelect, }: ModelPickerProps): React.ReactElement;
25
+ export declare function ModelPicker({ isActive, registry, currentProvider, currentModel, maxRows, cols, theme, caps, onSelect, }: ModelPickerProps): React.ReactElement;
29
26
  export {};
@@ -1,10 +1,19 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import SelectInput from 'ink-select-input';
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Model picker overlay. Sources its provider set from the adapter-backed
4
+ * providers only (R5) so it never offers a model whose provider would fail at
5
+ * run with `Unknown LLM provider`. Lists each provider's builtin models.
6
+ *
7
+ * Selection belongs to a model ID, independently of the visible row window.
8
+ * Resizing or hiding the overlay never resets it. Each item occupies one row;
9
+ * this component owns navigation while App owns Escape.
10
+ */
11
+ import { useState } from 'react';
12
+ import { Text, useInput } from 'ink';
3
13
  import { ADAPTER_PROVIDERS } from '../../config/schema.js';
4
14
  import { pickGlyphs } from '../glyphs.js';
5
- import { OverlayFrame } from '../layout/OverlayFrame.js';
6
- import { overlayListLimit } from '../layout/overlay-window.js';
7
- export function ModelPicker({ registry, currentProvider, currentModel, maxRows, cols, theme, caps, onSelect, }) {
15
+ import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
16
+ export function ModelPicker({ isActive = true, registry, currentProvider, currentModel, maxRows, cols, theme, caps, onSelect, }) {
8
17
  const glyphs = pickGlyphs(caps);
9
18
  const items = [];
10
19
  const seen = new Set();
@@ -21,9 +30,34 @@ export function ModelPicker({ registry, currentProvider, currentModel, maxRows,
21
30
  });
22
31
  }
23
32
  }
24
- const initialIndex = Math.max(0, items.findIndex((it) => it.value === `${currentProvider}:${currentModel}`));
25
- return (_jsx(OverlayFrame, { title: "Select a model", hint: `Enter choose ${glyphs.midDot} Esc cancel`, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: _jsx(SelectInput, { items: items, limit: overlayListLimit(maxRows), initialIndex: initialIndex >= 0 ? initialIndex : 0, onSelect: (item) => {
26
- const [provider, ...rest] = item.value.split(':');
27
- onSelect(provider, rest.join(':'));
28
- } }) }));
33
+ const [selectedValue, setSelectedValue] = useState(`${currentProvider}:${currentModel}`);
34
+ const selectedIndex = Math.max(0, items.findIndex(item => item.value === selectedValue));
35
+ const visibleRows = overlayBodyRows(maxRows, cols);
36
+ const offset = Math.max(0, Math.min(selectedIndex - visibleRows + 1, items.length - visibleRows));
37
+ const choose = (item) => {
38
+ if (!item)
39
+ return;
40
+ const [provider, ...rest] = item.value.split(':');
41
+ onSelect(provider, rest.join(':'));
42
+ };
43
+ useInput((input, key) => {
44
+ // Consume hidden input without changing selection, including when this is
45
+ // the only mounted input handler; otherwise buffered keys can replay later.
46
+ if (!isActive || !items.length)
47
+ return;
48
+ if (key.upArrow || input === 'k') {
49
+ setSelectedValue(items[(selectedIndex - 1 + items.length) % items.length].value);
50
+ }
51
+ else if (key.downArrow || input === 'j') {
52
+ setSelectedValue(items[(selectedIndex + 1) % items.length].value);
53
+ }
54
+ else if (key.return)
55
+ choose(items[selectedIndex]);
56
+ else if (/^[1-9]$/.test(input)) {
57
+ const visibleIndex = Number(input) - 1;
58
+ if (visibleIndex < visibleRows)
59
+ choose(items[offset + visibleIndex]);
60
+ }
61
+ });
62
+ return (_jsx(OverlayFrame, { title: "Select a model", hint: `Enter choose ${glyphs.midDot} Esc cancel`, maxRows: maxRows, cols: cols, theme: theme, caps: caps, scrollOffset: offset, rows: items.length ? items.map((item, index) => _jsxs(Text, { wrap: "truncate", color: index === selectedIndex ? theme.accent : theme.primary, children: [index === selectedIndex ? `${glyphs.caret} ` : ' ', item.label] }, item.value)) : [_jsx(Text, { wrap: "truncate", children: "No models available" }, "empty")] }));
29
63
  }
@@ -24,6 +24,8 @@ function useEditorInput(props, navigation) {
24
24
  dispatch({ type: 'merge-editor' });
25
25
  }
26
26
  useInput((input, key) => {
27
+ if (props.isActive === false)
28
+ return;
27
29
  if (key.escape) {
28
30
  dispatch({ type: 'cancel-editor' });
29
31
  return;
@@ -3,6 +3,7 @@ import type { ModelProfile, ModelProfilesConfig, ModelRole } from '../../config/
3
3
  import type { Theme } from '../theme.js';
4
4
  import type { TermCapabilities } from '../capabilities.js';
5
5
  export interface ProfileFrameProps {
6
+ isActive?: boolean;
6
7
  theme: Theme;
7
8
  caps: TermCapabilities;
8
9
  cols: number;
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
3
3
  import { Text, useInput } from 'ink';
4
4
  import stringWidth from 'string-width';
5
5
  import { pickGlyphs } from '../glyphs.js';
6
- import { OverlayFrame } from '../layout/OverlayFrame.js';
6
+ import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
7
7
  import { stripPasteFrames } from '../paste-frames.js';
8
8
  import { stripEnterFrames } from '../enter-frames.js';
9
9
  import { profileUsers } from './model-profile-state.js';
@@ -13,7 +13,7 @@ export function useProfileScroll(props, focus) {
13
13
  useEffect(() => { props.onScrollClamp?.(0); }, []);
14
14
  useEffect(() => { setOffset(props.scrollOffset); }, [props.scrollOffset]);
15
15
  useEffect(() => {
16
- const height = Math.max(1, props.maxRows - (props.cols >= 48 ? 5 : 3));
16
+ const height = overlayBodyRows(props.maxRows, props.cols);
17
17
  const next = focus < offset ? focus : focus >= offset + height ? focus - height + 1 : offset;
18
18
  if (next !== offset) {
19
19
  setOffset(next);
@@ -98,6 +98,8 @@ function usePickerInput(props, state) {
98
98
  props.onDelete(selected.id);
99
99
  }
100
100
  useInput((input, key) => {
101
+ if (props.isActive === false)
102
+ return;
101
103
  if (key.escape) {
102
104
  if (searching)
103
105
  setSearching(false);
@@ -30,6 +30,7 @@ export type PlanVerdict = {
30
30
  feedback: string;
31
31
  };
32
32
  interface PlanReviewOverlayProps {
33
+ isActive?: boolean;
33
34
  plan: NormalizedPlan;
34
35
  maxRows: number;
35
36
  cols: number;
@@ -39,7 +40,7 @@ interface PlanReviewOverlayProps {
39
40
  caps: TermCapabilities;
40
41
  onVerdict: (verdict: PlanVerdict) => void;
41
42
  }
42
- export declare function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }: PlanReviewOverlayProps): React.ReactElement;
43
+ export declare function PlanReviewOverlay({ isActive, plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }: PlanReviewOverlayProps): React.ReactElement;
43
44
  /**
44
45
  * Flatten the plan into one-row elements.
45
46
  *
@@ -25,16 +25,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
25
  import { useState } from 'react';
26
26
  import { Text, useInput } from 'ink';
27
27
  import { pickGlyphs } from '../glyphs.js';
28
- import { OverlayFrame } from '../layout/OverlayFrame.js';
28
+ import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
29
29
  import { wrapToRows } from '../layout/wrap-rows.js';
30
30
  import { stripPasteFrames } from '../paste-frames.js';
31
31
  import { stripEnterFrames } from '../enter-frames.js';
32
32
  /** The frame's border plus its one-column padding on each side. */
33
33
  const FRAME_CHROME_COLS = 4;
34
- export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }) {
34
+ export function PlanReviewOverlay({ isActive = true, plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }) {
35
35
  const glyphs = pickGlyphs(caps);
36
36
  const [feedback, setFeedback] = useState(null);
37
37
  useInput((input, key) => {
38
+ if (!isActive)
39
+ return;
38
40
  if (key.escape)
39
41
  return; // App owns dismiss.
40
42
  if (feedback !== null) {
@@ -84,9 +86,10 @@ export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollC
84
86
  // whole review away from either sub-state. `<-` is the route back to the
85
87
  // card, which is why it is named here too.
86
88
  const feedbackHint = `${glyphs.enterKey} send ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc dismiss`;
87
- return (_jsxs(OverlayFrame, { title: "What should change?", hint: feedbackHint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [_jsx(Text, { wrap: "truncate", color: theme.muted, children: "The agent revises the plan and submits it again." }), _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.primary, bold: true, children: [glyphs.caret, ' '] }), feedback.length > 0 ? (_jsx(Text, { children: feedback })) : (_jsx(Text, { color: theme.muted, children: "e.g. use the existing session store instead" })), _jsx(Text, { inverse: true, children: " " })] })] }));
89
+ return (_jsxs(OverlayFrame, { title: "What should change?", hint: feedbackHint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: [overlayBodyRows(maxRows, cols) >= 2 && _jsx(Text, { wrap: "truncate", color: theme.muted, children: "The agent revises the plan and submits it again." }), _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.primary, bold: true, children: [glyphs.caret, ' '] }), feedback.length > 0 ? (_jsx(Text, { children: wrapToRows(feedback, Math.max(1, cols - FRAME_CHROME_COLS - 3)).at(-1) })) : (_jsx(Text, { color: theme.muted, children: "type feedback" })), _jsx(Text, { inverse: true, children: " " })] })] }));
88
90
  }
89
- return (_jsx(OverlayFrame, { title: "Review plan", hint: `a approve ${glyphs.midDot} r revise ${glyphs.midDot} esc dismiss ${glyphs.midDot} ${glyphs.arrowUp}${glyphs.arrowDown} scroll`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
91
+ return (_jsx(OverlayFrame, { title: "Review plan", hint: cols <= 48 ? "a approve r revise Esc cancel"
92
+ : `a approve ${glyphs.midDot} r revise ${glyphs.midDot} esc dismiss ${glyphs.midDot} ${glyphs.arrowUp}${glyphs.arrowDown} scroll`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
90
93
  }
91
94
  /**
92
95
  * Flatten the plan into one-row elements.
@@ -19,6 +19,7 @@ import type { Theme } from '../theme.js';
19
19
  import type { TermCapabilities } from '../capabilities.js';
20
20
  import type { Answer, NormalizedQuestion } from '../../tools/human-input.js';
21
21
  interface QuestionOverlayProps {
22
+ isActive?: boolean;
22
23
  questions: NormalizedQuestion[];
23
24
  maxRows: number;
24
25
  cols: number;
@@ -26,5 +27,5 @@ interface QuestionOverlayProps {
26
27
  caps: TermCapabilities;
27
28
  onSubmit: (answers: Answer[]) => void;
28
29
  }
29
- export declare function QuestionOverlay({ questions, maxRows, cols, theme, caps, onSubmit, }: QuestionOverlayProps): React.ReactElement;
30
+ export declare function QuestionOverlay({ isActive, questions, maxRows, cols, theme, caps, onSubmit, }: QuestionOverlayProps): React.ReactElement;
30
31
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * QuestionOverlay — the keyboard-driven multiple-choice wizard `ask_user`
4
4
  * renders (plan-mode §6.5).
@@ -15,10 +15,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
15
15
  * opposite — both it and `App` call `resolve` — which is harmless there only by
16
16
  * accident of promise semantics, and is not a pattern to copy (R-P12).
17
17
  */
18
- import { useState } from 'react';
19
- import { Box, Text, useInput } from 'ink';
18
+ import { useState, useLayoutEffect } from 'react';
19
+ import { Text, useInput } from 'ink';
20
20
  import { pickGlyphs } from '../glyphs.js';
21
- import { OverlayFrame } from '../layout/OverlayFrame.js';
21
+ import { wrapToRows } from '../layout/wrap-rows.js';
22
+ import { OverlayFrame, overlayBodyRows } from '../layout/OverlayFrame.js';
22
23
  import { stripPasteFrames } from '../paste-frames.js';
23
24
  import { stripEnterFrames } from '../enter-frames.js';
24
25
  /** Which option index each question starts on: the recommended one. */
@@ -31,7 +32,7 @@ function initialSelections(questions) {
31
32
  return new Set(recommended >= 0 ? [recommended] : [0]);
32
33
  });
33
34
  }
34
- export function QuestionOverlay({ questions, maxRows, cols, theme, caps, onSubmit, }) {
35
+ export function QuestionOverlay({ isActive = true, questions, maxRows, cols, theme, caps, onSubmit, }) {
35
36
  const glyphs = pickGlyphs(caps);
36
37
  const [step, setStep] = useState(0);
37
38
  const [phase, setPhase] = useState('choosing');
@@ -39,6 +40,7 @@ export function QuestionOverlay({ questions, maxRows, cols, theme, caps, onSubmi
39
40
  const [selections, setSelections] = useState(() => initialSelections(questions));
40
41
  const [customs, setCustoms] = useState(() => questions.map(() => ''));
41
42
  const [draft, setDraft] = useState('');
43
+ const [offset, setOffset] = useState(0);
42
44
  const question = questions[Math.min(step, questions.length - 1)];
43
45
  const cursor = cursors[step] ?? 0;
44
46
  const setCursorAt = (index, value) => setCursors((prev) => prev.map((c, i) => (i === index ? value : c)));
@@ -80,6 +82,16 @@ export function QuestionOverlay({ questions, maxRows, cols, theme, caps, onSubmi
80
82
  setPhase('choosing');
81
83
  };
82
84
  useInput((input, key) => {
85
+ if (!isActive)
86
+ return;
87
+ if (key.pageUp) {
88
+ setOffset(n => Math.max(0, n - 1));
89
+ return;
90
+ }
91
+ if (key.pageDown) {
92
+ setOffset(n => n + 1);
93
+ return;
94
+ }
83
95
  // App owns Esc for both new overlays (R-P12). Swallow it here so the
84
96
  // free-text editor below cannot eat the raw escape byte as a character.
85
97
  if (key.escape)
@@ -172,31 +184,32 @@ export function QuestionOverlay({ questions, maxRows, cols, theme, caps, onSubmi
172
184
  : phase === 'review'
173
185
  ? `${glyphs.enterKey} submit ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc cancel`
174
186
  : `${glyphs.arrowUp}${glyphs.arrowDown} choose ${glyphs.midDot} ${glyphs.enterKey} next${question.allowMultiple ? ` ${glyphs.midDot} space toggle` : ''} ${glyphs.midDot} ${glyphs.arrowLeft} back ${glyphs.midDot} esc cancel`;
175
- return (_jsx(OverlayFrame, { title: `${title}${position}`, hint: hint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, children: phase === 'review' ? (_jsx(ReviewBody, { questions: questions, answers: buildAnswers(), theme: theme })) : (_jsx(QuestionBody, { question: question, cursor: cursor, selected: selections[step] ?? new Set(), typing: phase === 'typingOther', draft: draft, theme: theme, caps: caps })) }));
176
- }
177
- /**
178
- * ONE QUESTION PER SCREEN, not all of them at once. Three questions x four
179
- * options x a description line is 20+ rows against a ~15-row viewport on a
180
- * 24-row terminal.
181
- */
182
- function QuestionBody({ question, cursor, selected, typing, draft, theme, caps, }) {
183
- const glyphs = pickGlyphs(caps);
184
- const markOn = question.allowMultiple ? glyphs.boxChecked : glyphs.keyOn;
185
- const markOff = question.allowMultiple ? glyphs.boxEmpty : glyphs.keyOff;
186
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: question.header }), _jsx(Text, { wrap: "truncate", color: theme.assistant, children: question.question }), _jsx(Text, { children: " " }), question.options.map((option, i) => {
187
- const active = i === cursor;
188
- const on = selected.has(i);
189
- return (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: active ? theme.primary : theme.muted, children: [active ? glyphs.caret : ' ', ' '] }), _jsxs(Text, { color: on ? theme.primary : theme.muted, children: [on ? markOn : markOff, " "] }), _jsx(Text, { color: active ? theme.assistant : undefined, bold: active, children: option.isOther ? `${option.label}${glyphs.ellipsis}` : option.label }), option.description.length > 0 && (_jsxs(Text, { color: theme.muted, children: [' ', option.description] })), option.recommended && (_jsxs(Text, { backgroundColor: theme.chip.bg, color: theme.chip.fg, children: [' ', "RECOMMENDED", ' '] }))] }, `${question.id}-${i}`));
190
- }), typing && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { color: theme.primary, bold: true, children: [glyphs.caret, ' '] }), draft.length > 0 ? (_jsx(Text, { children: draft })) : (_jsx(Text, { color: theme.muted, children: "type your own answer" })), _jsx(Text, { inverse: true, children: " " })] })] }))] }));
191
- }
192
- /**
193
- * The review step exists because a wizard that fires on the last `Enter` with no
194
- * confirmation makes a mis-keyed answer unrecoverable.
195
- */
196
- function ReviewBody({ questions, answers, theme, }) {
197
- return (_jsx(Box, { flexDirection: "column", children: questions.map((q, i) => {
198
- const answer = answers[i];
199
- const value = answer?.custom ?? answer?.selected.join(', ') ?? '';
200
- return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.accent, children: q.header.padEnd(14) }), _jsx(Text, { color: theme.assistant, children: value })] }, q.id));
201
- }) }));
187
+ const rowTexts = [];
188
+ const optionStarts = [];
189
+ const push = (text) => { rowTexts.push(...wrapToRows(text, Math.max(1, cols - 4))); };
190
+ if (phase === 'review') {
191
+ buildAnswers().forEach((answer, index) => push(`${questions[index].header}: ${answer.custom ?? answer.selected.join(', ')}`));
192
+ }
193
+ else {
194
+ push(question.header);
195
+ push(question.question);
196
+ question.options.forEach((option, index) => {
197
+ optionStarts.push(rowTexts.length);
198
+ const mark = (selections[step] ?? new Set()).has(index) ? '[x]' : '[ ]';
199
+ push(`${index === cursor ? glyphs.caret : ' '} ${mark} ${option.label}${option.recommended ? ' RECOMMENDED' : ''} ${option.description}`);
200
+ });
201
+ if (phase === 'typingOther')
202
+ push(`${glyphs.caret} ${draft || 'type your own answer'} `);
203
+ }
204
+ const focus = phase === 'review' ? 0 : phase === 'typingOther'
205
+ ? Math.max(0, rowTexts.length - 1) : optionStarts[cursor] ?? 0;
206
+ useLayoutEffect(() => {
207
+ const height = overlayBodyRows(maxRows, cols);
208
+ setOffset(previous => focus < previous ? focus : focus >= previous + height
209
+ ? focus - height + 1 : previous);
210
+ }, [focus, maxRows, cols, step, phase]);
211
+ const shortHint = phase === 'typingOther' ? 'Enter commit | Left back | Esc cancel'
212
+ : phase === 'review' ? 'Enter submit | Left back | Esc cancel'
213
+ : question.allowMultiple ? '^v choose Space toggle Enter next Esc' : 'Up/Down choose Enter next Esc cancel';
214
+ return _jsx(OverlayFrame, { title: `${title}${position}`, hint: cols <= 48 ? shortHint : hint, maxRows: maxRows, cols: cols, theme: theme, caps: caps, rows: rowTexts.map((line, index) => _jsx(Text, { wrap: "truncate", children: line || ' ' }, index)), scrollOffset: offset, onScrollClamp: setOffset });
202
215
  }
@@ -74,6 +74,6 @@ export function QueueOverlay(props) {
74
74
  onScrollClamp(offset);
75
75
  }, [projected, offset, scrollOffset, onScrollClamp]);
76
76
  const rows = useMemo(() => projected.length ? projected.map((row, index) => _jsx(Text, { wrap: "truncate", color: theme.primary, children: row.text || ' ' }, `${row.queueId}:${index}`))
77
- : [_jsx(Text, { children: '\u961f\u5217\u5df2\u5904\u7406\u5b8c\u6bd5' }, "empty")], [projected, theme]);
78
- return _jsx(OverlayFrame, { ...props, title: `Queue: \u5f85\u5904\u7406 ${pending.length}`, hint: "PgUp/PgDn | Up/Down | Esc", rows: rows, scrollOffset: offset });
77
+ : [_jsx(Text, { children: "Queue is empty" }, "empty")], [projected, theme]);
78
+ return _jsx(OverlayFrame, { ...props, title: `Queue: ${pending.length} pending`, hint: "PgUp/PgDn | Up/Down | Esc", rows: rows, scrollOffset: offset });
79
79
  }
@@ -87,6 +87,7 @@ export interface SettingsValues {
87
87
  compactionArchive: 'on' | 'off';
88
88
  }
89
89
  interface SettingsScreenProps {
90
+ isActive?: boolean;
90
91
  initial: SettingsValues;
91
92
  /** Resolved keys per provider so switching providers reloads the right key. */
92
93
  apiKeys: Record<string, string | undefined>;
@@ -247,11 +247,12 @@ export function SettingsScreen(props) {
247
247
  return props.draft ? _jsx(ProfileSettingsScreen, { ...props, draft: props.draft })
248
248
  : _jsx(LegacySettingsScreen, { ...props });
249
249
  }
250
- function LegacySettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, retrySummary, }) {
250
+ function LegacySettingsScreen({ isActive = true, initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, retrySummary, }) {
251
251
  const [values, setValues] = useState(initial);
252
252
  const [index, setIndex] = useState(0);
253
253
  const glyphs = pickGlyphs(caps);
254
254
  const field = FIELDS[index];
255
+ const focusedOffset = useProfileScroll({ maxRows, cols, scrollOffset, onScrollClamp, theme, caps }, index);
255
256
  const cycle = (dir) => {
256
257
  if (field.kind !== 'enum' || !field.options)
257
258
  return;
@@ -271,6 +272,8 @@ function LegacySettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, o
271
272
  });
272
273
  };
273
274
  useInput((input, key) => {
275
+ if (!isActive)
276
+ return;
274
277
  if (key.escape)
275
278
  return; // App closes the overlay.
276
279
  if (key.upArrow) {
@@ -380,7 +383,7 @@ function LegacySettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, o
380
383
  rows.push(_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', 'API retries'.padEnd(18), retrySummary] }, "__retry"));
381
384
  }
382
385
  rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
383
- return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
386
+ return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: focusedOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
384
387
  }
385
388
  const PROFILE_FIELDS = FIELDS.filter((field) => ![
386
389
  'provider', 'model', 'baseUrl', 'apiKey', 'fastProvider', 'fastModel', 'fastEnabled',
@@ -592,6 +595,8 @@ function activateRootSetting(props, view) {
592
595
  function useRootSettingsInput(props, view) {
593
596
  const { index, count, field, readError, persisted, values, state, setIndex, setValues } = view;
594
597
  useInput((input, key) => {
598
+ if (props.isActive === false)
599
+ return;
595
600
  if (key.escape) {
596
601
  cancelProfileSettings(props, view);
597
602
  return;
@@ -635,6 +640,8 @@ function useRootSettingsInput(props, view) {
635
640
  function useSettingsConfirmationInput(props, view) {
636
641
  const { state, dispatch, confirmIndex, setConfirmIndex, reloadPending, confirming } = view;
637
642
  useInput((_input, key) => {
643
+ if (props.isActive === false)
644
+ return;
638
645
  if (key.escape) {
639
646
  dispatch({ type: 'page', page: state.page === 'delete-confirm'
640
647
  ? 'manager' : 'root' });
@@ -664,7 +671,7 @@ function renderSettingsConfirmation(props, view) {
664
671
  const name = library.entries.find((entry) => entry.id === state.deleteId)?.name ?? '';
665
672
  const labels = deleting ? ['Cancel', 'Delete'] : ['Continue', 'Discard'];
666
673
  return _jsx(OverlayFrame, { ...props, title: deleting ? `Delete ${name}?` : 'Discard unsaved changes?', hint: "Enter choose | Esc continue", rows: labels
667
- .map((label, at) => _jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label)), scrollOffset: 0 });
674
+ .map((label, at) => _jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [confirmIndex === at ? `${glyphs.caret} ` : ' ', label] }, label)), scrollOffset: confirmIndex });
668
675
  }
669
676
  function renderProfileSubpage(props, view) {
670
677
  const { state, library, dispatch, setConfirmIndex } = view;
@@ -12,7 +12,7 @@
12
12
  * - `Markdown` re-split and re-regexed the whole answer into one React element
13
13
  * per line on every render.
14
14
  *
15
- * Everything cached here is a function of its INPUT STRING ALONE. Theme and
15
+ * Cached text depends on its input and, for highlighting, the color level. Theme and
16
16
  * glyphs are applied at render time and are never cached, because a themed
17
17
  * React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
18
18
  * exactly the class of silent bug this package writes comments to avoid.
@@ -50,7 +50,7 @@ export interface RenderCacheStats {
50
50
  misses: number;
51
51
  }
52
52
  /**
53
- * Syntax-highlight `code`, memoised on `(lang, code)`.
53
+ * Syntax-highlight `code`, memoised on `(colorLevel, lang, code)`.
54
54
  *
55
55
  * `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
56
56
  * unlike a rendered element this string IS safe to cache across a `/theme`
@@ -12,7 +12,7 @@
12
12
  * - `Markdown` re-split and re-regexed the whole answer into one React element
13
13
  * per line on every render.
14
14
  *
15
- * Everything cached here is a function of its INPUT STRING ALONE. Theme and
15
+ * Cached text depends on its input and, for highlighting, the color level. Theme and
16
16
  * glyphs are applied at render time and are never cached, because a themed
17
17
  * React element in a cache makes `/theme` a no-op until the entry evicts (K-5) —
18
18
  * exactly the class of silent bug this package writes comments to avoid.
@@ -22,6 +22,7 @@
22
22
  * caches would miss precisely when a scroll brings an old entry back.
23
23
  */
24
24
  import { highlight } from 'cli-highlight';
25
+ import { syntaxColorLevel } from './color-runtime.js';
25
26
  import { parseMarkdownBlocks } from './markdown-blocks.js';
26
27
  import { pickGlyphs } from './glyphs.js';
27
28
  export const RENDER_CACHE_LIMITS = {
@@ -113,7 +114,7 @@ const markdownCache = new LruCache(RENDER_CACHE_LIMITS.markdownEntries, RENDER_C
113
114
  */
114
115
  const lineCache = new LruCache(RENDER_CACHE_LIMITS.linesEntries, RENDER_CACHE_LIMITS.markdownBytes);
115
116
  /**
116
- * Syntax-highlight `code`, memoised on `(lang, code)`.
117
+ * Syntax-highlight `code`, memoised on `(colorLevel, lang, code)`.
117
118
  *
118
119
  * `cli-highlight` emits chalk-level ANSI that does not depend on `theme.*`, so
119
120
  * unlike a rendered element this string IS safe to cache across a `/theme`
@@ -129,7 +130,7 @@ export function highlightCached(code, lang) {
129
130
  // ripgrep, which silently hides every definition in it from the tool this
130
131
  // codebase navigates itself with. NUL rather than a space because a space
131
132
  // still lets ("a b", "ts") and ("b", "ts a") collide on one key.
132
- const key = `${lang}\u0000${code}`;
133
+ const key = `${syntaxColorLevel()}\u0000${lang}\u0000${code}`;
133
134
  const hit = highlightCache.get(key);
134
135
  if (hit !== undefined)
135
136
  return hit;
@@ -0,0 +1,23 @@
1
+ /** UI-only metrics. Feed only the existing generation-guarded main subscription. */
2
+ export interface RunMetrics {
3
+ runId: number;
4
+ startedAt: number | null;
5
+ endedAt: number | null;
6
+ outputTokens: number;
7
+ usageKnown: boolean;
8
+ }
9
+ export interface RunMetricsEvent {
10
+ type: string;
11
+ usage?: {
12
+ outputTokens?: number;
13
+ };
14
+ }
15
+ export interface RunMetricsProjection {
16
+ elapsedMs: number;
17
+ tokPerSec: number;
18
+ speedKnown: boolean;
19
+ }
20
+ export declare function createRunMetrics(): RunMetrics;
21
+ export declare function freezeRunMetrics(state: RunMetrics, now: number): RunMetrics;
22
+ export declare function updateRunMetrics(state: RunMetrics, event: RunMetricsEvent, now: number): RunMetrics;
23
+ export declare function projectRunMetrics(state: RunMetrics, now: number): RunMetricsProjection;
@@ -0,0 +1,27 @@
1
+ export function createRunMetrics() {
2
+ return { runId: 0, startedAt: null, endedAt: null, outputTokens: 0, usageKnown: false };
3
+ }
4
+ export function freezeRunMetrics(state, now) {
5
+ if (state.startedAt === null || state.endedAt !== null)
6
+ return state;
7
+ return { ...state, endedAt: Number.isFinite(now) ? Math.max(state.startedAt, now) : state.startedAt };
8
+ }
9
+ export function updateRunMetrics(state, event, now) {
10
+ if (event.type === 'agent_start')
11
+ return { ...createRunMetrics(), runId: state.runId + 1,
12
+ startedAt: Number.isFinite(now) ? now : 0 };
13
+ if (state.startedAt === null || state.endedAt !== null)
14
+ return state;
15
+ if (event.type === 'agent_end' || event.type === 'error')
16
+ return freezeRunMetrics(state, now);
17
+ const output = event.usage?.outputTokens;
18
+ if (event.type !== 'turn_end' || output === undefined || !Number.isFinite(output) || output < 0)
19
+ return state;
20
+ return { ...state, outputTokens: Math.min(Number.MAX_VALUE, state.outputTokens + output), usageKnown: true };
21
+ }
22
+ export function projectRunMetrics(state, now) {
23
+ const end = state.endedAt ?? (Number.isFinite(now) ? now : state.startedAt ?? 0);
24
+ const elapsedMs = state.startedAt === null ? 0 : Math.max(0, end - state.startedAt);
25
+ const speedKnown = state.startedAt !== null && state.endedAt === null && state.usageKnown && elapsedMs > 500;
26
+ return { elapsedMs, speedKnown, tokPerSec: speedKnown ? state.outputTokens / (elapsedMs / 1000) : 0 };
27
+ }
@@ -0,0 +1,33 @@
1
+ /** Facts, never translated toast text, determine action ownership and feedback. */
2
+ export interface StatusFeedback {
3
+ kind: 'confirm' | 'stopping' | 'force-stop' | 'exit' | 'copying' | 'copied' | 'copy-sent' | 'copy-cleanup' | 'copy-error' | 'notice';
4
+ text: string;
5
+ level: 'info' | 'warn' | 'error';
6
+ }
7
+ export type EscapeAction = 'menu' | 'cancel-start' | 'confirm-stop' | 'force-stop';
8
+ export interface StatusFeedbackInput {
9
+ interactionPhase: 'idle' | 'starting' | 'running';
10
+ interruptPhase?: 'ready' | 'armed' | 'stopping';
11
+ completion?: 'none' | 'slash' | 'file';
12
+ overlay?: string | null;
13
+ copyState?: {
14
+ busy: boolean;
15
+ cleanupPending: boolean;
16
+ };
17
+ copyResult?: {
18
+ status: 'confirmed' | 'sent' | 'error';
19
+ text: string;
20
+ };
21
+ ctrlCArmed?: boolean;
22
+ selectionPending?: boolean;
23
+ liveServices?: number;
24
+ toast?: {
25
+ text: string;
26
+ level: 'info' | 'warn' | 'error';
27
+ };
28
+ }
29
+ export interface StatusFeedbackProjection {
30
+ feedback?: StatusFeedback;
31
+ escapeAction?: EscapeAction;
32
+ }
33
+ export declare function projectStatusFeedback(input: StatusFeedbackInput): StatusFeedbackProjection;