@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.
- package/CHANGELOG.md +41 -0
- package/README.md +250 -222
- package/dist/agent/controller.d.ts +36 -2
- package/dist/agent/controller.js +244 -57
- package/dist/agent/model-profile-settings.js +11 -2
- package/dist/agent/plan-prompt.d.ts +1 -1
- package/dist/agent/plan-prompt.js +4 -3
- package/dist/agent/project-guidance-prompt.d.ts +4 -0
- package/dist/agent/project-guidance-prompt.js +104 -0
- package/dist/agent/reducer.d.ts +2 -0
- package/dist/agent/reducer.js +5 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/boot/guard.d.ts +3 -3
- package/dist/boot/guard.js +3 -3
- package/dist/cli.js +33 -15
- package/dist/commands/builtins.js +2 -2
- package/dist/compaction/child.d.ts +9 -1
- package/dist/compaction/child.js +43 -13
- package/dist/compaction/command.js +20 -10
- package/dist/compaction/compactor.d.ts +33 -204
- package/dist/compaction/compactor.js +311 -723
- package/dist/compaction/context-command.js +5 -3
- package/dist/compaction/digest.d.ts +15 -51
- package/dist/compaction/digest.js +113 -148
- package/dist/compaction/limits.d.ts +19 -142
- package/dist/compaction/limits.js +20 -147
- package/dist/compaction/memory-identity.d.ts +16 -0
- package/dist/compaction/memory-identity.js +63 -0
- package/dist/compaction/memory-input.d.ts +25 -0
- package/dist/compaction/memory-input.js +106 -0
- package/dist/compaction/memory-render.d.ts +4 -0
- package/dist/compaction/memory-render.js +47 -0
- package/dist/compaction/memory-validation.d.ts +21 -0
- package/dist/compaction/memory-validation.js +268 -0
- package/dist/compaction/memory.d.ts +79 -0
- package/dist/compaction/memory.js +118 -0
- package/dist/compaction/meter.d.ts +14 -10
- package/dist/compaction/meter.js +55 -19
- package/dist/compaction/operation.d.ts +18 -0
- package/dist/compaction/operation.js +68 -0
- package/dist/compaction/pressure.d.ts +6 -20
- package/dist/compaction/pressure.js +14 -23
- package/dist/compaction/summarize-call.d.ts +3 -16
- package/dist/compaction/summarize-call.js +20 -20
- package/dist/compaction/summary-prompt.d.ts +29 -66
- package/dist/compaction/summary-prompt.js +132 -178
- package/dist/compaction/types.d.ts +23 -3
- package/dist/compaction/wiring.d.ts +10 -25
- package/dist/compaction/wiring.js +60 -67
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +8 -4
- package/dist/config/schema.d.ts +3 -3
- package/dist/config/schema.js +1 -1
- package/dist/exec/events.d.ts +8 -0
- package/dist/exec/index.js +13 -6
- package/dist/exec/runner.d.ts +8 -0
- package/dist/exec/runner.js +26 -2
- package/dist/session/persist.d.ts +3 -0
- package/dist/session/persist.js +9 -0
- package/dist/session/store.js +2 -0
- package/dist/session/validate-session.js +34 -0
- package/dist/team/subagent.js +8 -0
- package/dist/ui/App.js +221 -126
- package/dist/ui/BottomStatusRow.d.ts +5 -10
- package/dist/ui/BottomStatusRow.js +7 -11
- package/dist/ui/Composer.d.ts +4 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +6 -2
- package/dist/ui/Header.js +7 -6
- package/dist/ui/Logo.d.ts +3 -5
- package/dist/ui/Logo.js +4 -8
- package/dist/ui/PromptInput.d.ts +3 -1
- package/dist/ui/PromptInput.js +35 -42
- package/dist/ui/StatusBar.d.ts +6 -0
- package/dist/ui/StatusBar.js +8 -7
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/capabilities.d.ts +2 -0
- package/dist/ui/capabilities.js +6 -0
- package/dist/ui/color-runtime.d.ts +5 -0
- package/dist/ui/color-runtime.js +29 -0
- package/dist/ui/entries/CompactionCard.d.ts +3 -1
- package/dist/ui/entries/CompactionCard.js +7 -5
- package/dist/ui/frame-differ.js +27 -0
- package/dist/ui/frame-observer.js +18 -5
- package/dist/ui/glyphs.js +10 -8
- package/dist/ui/ink-runtime.d.ts +3 -0
- package/dist/ui/ink-runtime.js +30 -0
- package/dist/ui/interaction-copy.d.ts +73 -73
- package/dist/ui/interaction-copy.js +30 -38
- package/dist/ui/interaction-hints.d.ts +8 -1
- package/dist/ui/interaction-hints.js +56 -69
- package/dist/ui/layout/AppShell.d.ts +6 -2
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/OverlayFrame.d.ts +2 -0
- package/dist/ui/layout/OverlayFrame.js +23 -14
- package/dist/ui/layout/ScrollViewport.d.ts +3 -1
- package/dist/ui/layout/ScrollViewport.js +10 -2
- package/dist/ui/layout/budget.d.ts +25 -26
- package/dist/ui/layout/budget.js +29 -22
- package/dist/ui/layout/document-layout.d.ts +2 -2
- package/dist/ui/layout/document-layout.js +2 -3
- package/dist/ui/layout/queue-layout.js +4 -4
- package/dist/ui/layout/status-detail-layout.d.ts +9 -0
- package/dist/ui/layout/status-detail-layout.js +56 -0
- package/dist/ui/layout/status-layout.d.ts +19 -2
- package/dist/ui/layout/status-layout.js +95 -83
- package/dist/ui/layout/wrap-rows.d.ts +2 -0
- package/dist/ui/layout/wrap-rows.js +21 -7
- package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
- package/dist/ui/overlays/ConfirmDialog.js +22 -3
- package/dist/ui/overlays/HelpOverlay.js +9 -2
- package/dist/ui/overlays/ModelPicker.d.ts +5 -8
- package/dist/ui/overlays/ModelPicker.js +44 -10
- package/dist/ui/overlays/ModelProfileEditor.js +2 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
- package/dist/ui/overlays/ModelProfilePicker.js +4 -2
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
- package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
- package/dist/ui/overlays/QuestionOverlay.js +45 -32
- package/dist/ui/overlays/QueueOverlay.js +2 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
- package/dist/ui/overlays/SettingsScreen.js +10 -3
- package/dist/ui/render-cache.d.ts +2 -2
- package/dist/ui/render-cache.js +4 -3
- package/dist/ui/run-metrics.d.ts +23 -0
- package/dist/ui/run-metrics.js +27 -0
- package/dist/ui/status-feedback.d.ts +33 -0
- package/dist/ui/status-feedback.js +36 -0
- package/dist/ui/stdout-frame-writer.js +28 -0
- package/package.json +2 -2
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
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 (
|
|
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
|
|
31
|
-
['Bottom rows', '
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
}
|
|
@@ -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 =
|
|
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: "
|
|
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:
|
|
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
|
|
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 {
|
|
18
|
+
import { useState, useLayoutEffect } from 'react';
|
|
19
|
+
import { Text, useInput } from 'ink';
|
|
20
20
|
import { pickGlyphs } from '../glyphs.js';
|
|
21
|
-
import {
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
78
|
-
return _jsx(OverlayFrame, { ...props, title: `Queue:
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
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`
|
package/dist/ui/render-cache.js
CHANGED
|
@@ -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
|
-
*
|
|
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;
|