@aragon-agent/cli 0.6.14 → 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 +14 -0
- package/README.md +250 -233
- package/dist/agent/controller.d.ts +27 -1
- package/dist/agent/controller.js +182 -45
- 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 +4 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/commands/builtins.js +2 -2
- package/dist/compaction/child.d.ts +9 -1
- package/dist/compaction/child.js +39 -13
- package/dist/compaction/command.js +20 -10
- package/dist/compaction/compactor.d.ts +33 -204
- package/dist/compaction/compactor.js +311 -726
- 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 +53 -19
- package/dist/compaction/operation.d.ts +18 -0
- package/dist/compaction/operation.js +68 -0
- package/dist/compaction/pressure.d.ts +5 -20
- package/dist/compaction/pressure.js +13 -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 +21 -3
- package/dist/compaction/wiring.d.ts +10 -25
- package/dist/compaction/wiring.js +56 -66
- package/dist/config/schema.d.ts +1 -1
- 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 +218 -116
- 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/PromptInput.d.ts +3 -1
- package/dist/ui/PromptInput.js +32 -41
- 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/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/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 -84
- 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/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,29 +1,28 @@
|
|
|
1
|
-
/** Fullscreen reserves three fixed rows; composer height is a footer layout input. */
|
|
2
|
-
/** Below this many rows the composer's hint line is dropped to buy back a row. */
|
|
3
1
|
export declare const HINT_MIN_ROWS = 20;
|
|
4
|
-
export interface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* The draft term used to be the constant 1 that the `3` / `4` below encode.
|
|
12
|
-
* That constant is what made a pasted 200-line draft draw the transcript 197
|
|
13
|
-
* rows shorter than every consumer of `viewportRows` believed -- the trap
|
|
14
|
-
* `BottomStatusRow.tsx:5-27` already documents at 1/200th the magnitude.
|
|
15
|
-
*/
|
|
16
|
-
composer: number;
|
|
17
|
-
status: number;
|
|
2
|
+
export interface FrameBudgetInput {
|
|
3
|
+
rows?: number;
|
|
4
|
+
cols?: number;
|
|
5
|
+
draftRows: number;
|
|
6
|
+
popupRows: number;
|
|
7
|
+
statusExpanded: boolean;
|
|
18
8
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
export interface FrameBudget {
|
|
10
|
+
rows: number;
|
|
11
|
+
cols: number;
|
|
12
|
+
inactive: boolean;
|
|
13
|
+
frameRows: number;
|
|
14
|
+
headerRows: number;
|
|
15
|
+
statusRows: 0 | 1 | 2;
|
|
16
|
+
composerRows: number;
|
|
17
|
+
composerSlotRows: number;
|
|
18
|
+
popupRows: number;
|
|
19
|
+
popupMaxHeight: number;
|
|
20
|
+
viewportRows: number;
|
|
21
|
+
composerCols: number;
|
|
22
|
+
}
|
|
23
|
+
/** Clamp the displayed draft, independently of any transcript or panel content. */
|
|
26
24
|
export declare function clampDraftRows(rows: number, draftRows: number): number;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
/** Total menu capacity, including its borders; reserve at least one message row. */
|
|
26
|
+
export declare function popupCapacity(rows: number, draftRows: number, statusExpanded: boolean): number;
|
|
27
|
+
/** Normalize terminal dimensions once and conserve every visible frame row. */
|
|
28
|
+
export declare function buildFrameBudget(input: FrameBudgetInput): FrameBudget;
|
package/dist/ui/layout/budget.js
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** One height budget for the transcript, fixed editor and status slots. */
|
|
2
2
|
import { draftMaxRows } from '../composer-limits.js';
|
|
3
|
-
import {
|
|
4
|
-
/** Below this many rows the composer's hint line is dropped to buy back a row. */
|
|
3
|
+
import { MIN_FULLSCREEN_ROWS, MIN_FULLSCREEN_COLS } from './frame.js';
|
|
5
4
|
export const HINT_MIN_ROWS = 20;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* Exported so the reporter and the budget clamp with ONE function: I-8 requires
|
|
10
|
-
* the rendered row count and the number handed to `viewportRows` to be the same
|
|
11
|
-
* number, and two clamps are how they stop being.
|
|
12
|
-
*/
|
|
5
|
+
const dimension = (value, fallback) => value !== undefined && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
|
|
6
|
+
/** Clamp the displayed draft, independently of any transcript or panel content. */
|
|
13
7
|
export function clampDraftRows(rows, draftRows) {
|
|
14
8
|
const wanted = Number.isFinite(draftRows) ? Math.floor(draftRows) : 1;
|
|
15
9
|
return Math.max(1, Math.min(wanted, draftMaxRows(rows)));
|
|
16
10
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
composer: 2 + clampDraftRows(rows, draftRows),
|
|
22
|
-
status: 1,
|
|
23
|
-
};
|
|
11
|
+
/** Total menu capacity, including its borders; reserve at least one message row. */
|
|
12
|
+
export function popupCapacity(rows, draftRows, statusExpanded) {
|
|
13
|
+
return Math.max(0, rows - 1 - 1 - (statusExpanded ? 2 : 1)
|
|
14
|
+
- 2 - clampDraftRows(rows, draftRows) - 1);
|
|
24
15
|
}
|
|
25
|
-
/**
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
/** Normalize terminal dimensions once and conserve every visible frame row. */
|
|
17
|
+
export function buildFrameBudget(input) {
|
|
18
|
+
const rows = dimension(input.rows, 24);
|
|
19
|
+
const cols = dimension(input.cols, 80);
|
|
20
|
+
const frameRows = rows - 1;
|
|
21
|
+
const composerCols = Math.max(0, cols - 1);
|
|
22
|
+
const inactive = rows < MIN_FULLSCREEN_ROWS || cols < MIN_FULLSCREEN_COLS;
|
|
23
|
+
if (inactive)
|
|
24
|
+
return { rows, cols, inactive, frameRows, composerCols, headerRows: 0,
|
|
25
|
+
statusRows: 0, composerRows: 0, composerSlotRows: 0, popupRows: 0,
|
|
26
|
+
popupMaxHeight: 0, viewportRows: 0 };
|
|
27
|
+
const statusRows = input.statusExpanded ? 2 : 1;
|
|
28
|
+
const composerRows = 2 + clampDraftRows(rows, input.draftRows);
|
|
29
|
+
const popupMaxHeight = popupCapacity(rows, input.draftRows, input.statusExpanded);
|
|
30
|
+
const requested = Number.isFinite(input.popupRows) ? Math.floor(input.popupRows) : 0;
|
|
31
|
+
const candidate = Math.max(0, Math.min(requested, popupMaxHeight));
|
|
32
|
+
const popupRows = candidate >= 3 ? candidate : 0;
|
|
33
|
+
const composerSlotRows = composerRows + popupRows;
|
|
34
|
+
return { rows, cols, inactive, frameRows, composerCols, headerRows: 1, statusRows,
|
|
35
|
+
composerRows, composerSlotRows, popupRows, popupMaxHeight,
|
|
36
|
+
viewportRows: frameRows - 1 - statusRows - composerSlotRows };
|
|
30
37
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface DocumentLayoutInput {
|
|
2
|
-
|
|
2
|
+
viewportRows: number;
|
|
3
3
|
bodyRows: number;
|
|
4
4
|
footerRows: number;
|
|
5
5
|
}
|
|
@@ -9,5 +9,5 @@ export interface DocumentLayout {
|
|
|
9
9
|
contentRows: number;
|
|
10
10
|
trailingContentRows: number;
|
|
11
11
|
}
|
|
12
|
-
/** Allocate the shared document, filling short sessions before the
|
|
12
|
+
/** Allocate the shared document, filling short sessions before the panels. */
|
|
13
13
|
export declare function buildDocumentLayout(input: DocumentLayoutInput): DocumentLayout;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
/** Allocate the shared document, filling short sessions before the editor. */
|
|
1
|
+
/** Allocate the shared document, filling short sessions before the panels. */
|
|
3
2
|
export function buildDocumentLayout(input) {
|
|
4
3
|
const count = (n) => Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
5
|
-
const viewportRows = count(input.
|
|
4
|
+
const viewportRows = count(input.viewportRows);
|
|
6
5
|
const body = count(input.bodyRows);
|
|
7
6
|
const footer = count(input.footerRows);
|
|
8
7
|
const paddingRows = Math.max(0, viewportRows - body - footer);
|
|
@@ -10,13 +10,13 @@ export function buildQueueLayout(input) {
|
|
|
10
10
|
const items = input.pending.slice(0, shown).map((item, index) => {
|
|
11
11
|
const prefix = `${index + 1}. `;
|
|
12
12
|
const lines = item.text.split(/\r\n|[\r\n]/).length;
|
|
13
|
-
const suffix = lines > 1 ? ` (+${lines - 1}
|
|
13
|
+
const suffix = lines > 1 ? ` (+${lines - 1} lines)` : '';
|
|
14
14
|
return { queueId: item.queueId, label: prefix + truncateSummary(queueSummary(item.text), Math.max(0, input.columns - stringWidth(prefix + suffix)), '...') + suffix };
|
|
15
15
|
});
|
|
16
16
|
const hiddenCount = count - items.length;
|
|
17
|
-
const paused = input.paused ? '
|
|
18
|
-
const hidden = hiddenCount ? `
|
|
19
|
-
const title = `Queue:
|
|
17
|
+
const paused = input.paused ? ' paused' : '';
|
|
18
|
+
const hidden = hiddenCount ? ` +${hiddenCount} more` : '';
|
|
19
|
+
const title = `Queue: ${count} pending${paused}${hidden} /queue`;
|
|
20
20
|
if (limit === 1)
|
|
21
21
|
return { rows: 1,
|
|
22
22
|
title: truncateSummary(`Queue ${count} /queue ${items[0].label}`, input.columns, '...'),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ActionHintInput } from '../interaction-hints.js';
|
|
2
|
+
import { type StatusLinePlan, type StatusPrimaryInput } from './status-layout.js';
|
|
3
|
+
export interface StatusDetailInput {
|
|
4
|
+
status: StatusPrimaryInput;
|
|
5
|
+
hints: ActionHintInput;
|
|
6
|
+
model: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function planStatusDetail(input: StatusDetailInput): StatusLinePlan;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { formatCost, formatTokens, promptTokensOf } from '../../agent/usage.js';
|
|
2
|
+
import { actionCandidates, clipStatusText, escapeClause } from '../interaction-hints.js';
|
|
3
|
+
import { statusField, statusLinePlan, validContext } from './status-layout.js';
|
|
4
|
+
export function planStatusDetail(input) {
|
|
5
|
+
const { status, hints } = input;
|
|
6
|
+
const budget = Math.max(0, Math.floor(status.columns) - 1);
|
|
7
|
+
const escape = escapeClause({ ...hints, escapeAction: status.escapeAction });
|
|
8
|
+
const reserved = [...(escape ? [statusField('escape', escape, 'warning')] : []),
|
|
9
|
+
statusField('toggle', hints.overlay ? 'Details on' : '^G less')];
|
|
10
|
+
const fields = [];
|
|
11
|
+
const add = (candidate) => {
|
|
12
|
+
if (statusLinePlan([...fields, candidate, ...reserved]).cells <= budget)
|
|
13
|
+
fields.push(candidate);
|
|
14
|
+
};
|
|
15
|
+
if (status.feedback) {
|
|
16
|
+
const remaining = budget - statusLinePlan(reserved).cells - (reserved.length ? 3 : 0);
|
|
17
|
+
const text = clipStatusText(status.feedback.text, Math.max(0, remaining));
|
|
18
|
+
if (text)
|
|
19
|
+
add(statusField('feedback', text, status.feedback.level === 'error' ? 'error'
|
|
20
|
+
: status.feedback.level === 'warn' ? 'warning' : 'normal'));
|
|
21
|
+
}
|
|
22
|
+
for (const [index, action] of actionCandidates(hints).entries())
|
|
23
|
+
add(statusField('action-' + index, action));
|
|
24
|
+
if (status.pendingCount > 0)
|
|
25
|
+
add(statusField('queue', 'Q' + status.pendingCount));
|
|
26
|
+
if (status.servicesActive?.live)
|
|
27
|
+
add(statusField('services', 'Svc' + status.servicesActive.live));
|
|
28
|
+
if (status.mode || status.pendingMode)
|
|
29
|
+
add(statusField('mode', status.pendingMode
|
|
30
|
+
? (status.mode ?? 'build') + '>' + status.pendingMode : status.mode));
|
|
31
|
+
if (validContext(status.context)) {
|
|
32
|
+
const approximate = status.context.source === 'estimate' || status.context.deltaTokens > 0 ? '~' : '';
|
|
33
|
+
add(statusField('context', 'Context ' + approximate + formatTokens(status.context.occupied)
|
|
34
|
+
+ '/' + formatTokens(status.context.window) + ' tokens'));
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
add(statusField('context', 'Context ? /context'));
|
|
38
|
+
if (status.speedKnown)
|
|
39
|
+
add(statusField('average', 'Avg: main output / run time'));
|
|
40
|
+
add(statusField('usage', 'Session input ' + formatTokens(promptTokensOf(status.usageTotal))
|
|
41
|
+
+ ' output ' + formatTokens(status.usageTotal.outputTokens)));
|
|
42
|
+
if (Number.isFinite(status.usageTotal.costUsd))
|
|
43
|
+
add(statusField('cost', 'Cost ' + formatCost(status.usageTotal.costUsd) + ' est'));
|
|
44
|
+
if (status.teamActive)
|
|
45
|
+
add(statusField('team', 'Team ' + status.teamActive.running + '/' + status.teamActive.total));
|
|
46
|
+
if (status.todoActive)
|
|
47
|
+
add(statusField('todo', 'Todo ' + status.todoActive.done + '/' + status.todoActive.total));
|
|
48
|
+
add(statusField('model', input.provider + ':' + input.model));
|
|
49
|
+
if (status.fastActive)
|
|
50
|
+
add(statusField('fast', 'Fast' + (status.fastActive.inFlight ? ' active' : '')));
|
|
51
|
+
if (status.ecoRung)
|
|
52
|
+
add(statusField('eco', 'Eco'));
|
|
53
|
+
if (hints.updateAvailable)
|
|
54
|
+
add(statusField('update', '/update'));
|
|
55
|
+
return statusLinePlan([...fields, ...reserved]);
|
|
56
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UsageTotal } from '../../agent/reducer.js';
|
|
2
2
|
import type { ContextUsageSnapshot } from '../../compaction/types.js';
|
|
3
|
+
import type { EscapeAction, StatusFeedback } from '../status-feedback.js';
|
|
3
4
|
export type RunPhase = 'idle' | 'starting' | 'waiting' | 'thinking' | 'generating' | 'preparing-tool' | 'tool';
|
|
4
5
|
export interface StatusLayoutInput {
|
|
5
6
|
columns: number;
|
|
@@ -43,13 +44,29 @@ export interface StatusLayoutInput {
|
|
|
43
44
|
};
|
|
44
45
|
scrolledLines?: number;
|
|
45
46
|
}
|
|
47
|
+
export interface StatusPrimaryInput extends StatusLayoutInput {
|
|
48
|
+
feedback?: StatusFeedback;
|
|
49
|
+
escapeAction?: EscapeAction;
|
|
50
|
+
speedKnown: boolean;
|
|
51
|
+
interruptPhase?: 'ready' | 'armed' | 'stopping';
|
|
52
|
+
}
|
|
46
53
|
export interface StatusField {
|
|
47
54
|
id: string;
|
|
48
55
|
text: string;
|
|
49
56
|
cells: number;
|
|
50
57
|
tone: 'normal' | 'muted' | 'warning' | 'error';
|
|
51
58
|
}
|
|
59
|
+
export interface StatusLinePlan {
|
|
60
|
+
fields: readonly StatusField[];
|
|
61
|
+
separator: string;
|
|
62
|
+
separatorCells: number;
|
|
63
|
+
cells: number;
|
|
64
|
+
}
|
|
52
65
|
export declare function cleanStatusText(text: string): string;
|
|
66
|
+
export declare function statusField(id: string, text: string, tone?: StatusField['tone']): StatusField;
|
|
67
|
+
export declare function statusLinePlan(fields: readonly StatusField[], separator?: string): StatusLinePlan;
|
|
53
68
|
export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive'>): readonly [string, string];
|
|
54
|
-
|
|
55
|
-
export declare function
|
|
69
|
+
export declare function validContext(context: ContextUsageSnapshot): boolean;
|
|
70
|
+
export declare function shortDuration(ms: number): string;
|
|
71
|
+
/** All five fields survive each width rung; only the redraw carrier is outside this plan. */
|
|
72
|
+
export declare function planPrimaryStatusFields(input: StatusPrimaryInput): StatusLinePlan;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import stringWidth from 'string-width';
|
|
2
2
|
import stripAnsi from 'strip-ansi';
|
|
3
|
-
import {
|
|
3
|
+
import { formatDuration } from '../../agent/usage.js';
|
|
4
4
|
import { interactionCopy as copy } from '../interaction-copy.js';
|
|
5
5
|
export function cleanStatusText(text) {
|
|
6
6
|
// eslint-disable-next-line no-control-regex
|
|
7
7
|
return stripAnsi(text).replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, ' ').trim();
|
|
8
8
|
}
|
|
9
|
+
export function statusField(id, text, tone = 'muted') {
|
|
10
|
+
const clean = cleanStatusText(text);
|
|
11
|
+
return { id, text: clean, cells: stringWidth(clean), tone };
|
|
12
|
+
}
|
|
13
|
+
export function statusLinePlan(fields, separator = ' | ') {
|
|
14
|
+
const separatorCells = stringWidth(separator);
|
|
15
|
+
return { fields, separator, separatorCells,
|
|
16
|
+
cells: fields.reduce((sum, item) => sum + item.cells, 0) + Math.max(0, fields.length - 1) * separatorCells };
|
|
17
|
+
}
|
|
9
18
|
export function phaseLabels(input) {
|
|
10
19
|
if (input.waitingForConfirmation)
|
|
11
20
|
return copy.confirming;
|
|
@@ -14,97 +23,99 @@ export function phaseLabels(input) {
|
|
|
14
23
|
if (input.compacting)
|
|
15
24
|
return copy.compacting;
|
|
16
25
|
if (input.retryActive)
|
|
17
|
-
return
|
|
18
|
-
if (input.activeTool)
|
|
19
|
-
|
|
20
|
-
if (name === 'read_file')
|
|
21
|
-
return copy.readFile;
|
|
22
|
-
if (name === 'write_file' || name === 'edit_file')
|
|
23
|
-
return copy.writeFile;
|
|
24
|
-
if (name === 'bash')
|
|
25
|
-
return copy.command;
|
|
26
|
-
return [copy.tool[0], `${copy.tool[1]} ${cleanStatusText(name)}`];
|
|
27
|
-
}
|
|
26
|
+
return copy.retrying;
|
|
27
|
+
if (input.activeTool)
|
|
28
|
+
return [copy.tool[0], copy.tool[1] + ' ' + cleanStatusText(input.activeTool.name)];
|
|
28
29
|
if (input.phase === 'idle' && input.runOutcome && input.runOutcome !== 'none')
|
|
29
30
|
return copy[input.runOutcome];
|
|
30
31
|
if (input.phase === 'preparing-tool')
|
|
31
32
|
return copy.preparingTool;
|
|
32
33
|
return copy[input.phase];
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const compactNumber = (value) => value >= 1e9 ? scientific(value) : formatTokens(value).replace(/\.0([kM])$/, '$1');
|
|
38
|
-
function contextText(context, detail = false, exponent = false) {
|
|
39
|
-
if (![context.occupied, context.window, context.deltaTokens, context.pct].every(Number.isFinite)
|
|
40
|
-
|| context.occupied < 0 || context.window <= 0)
|
|
41
|
-
return copy.contextUnknown;
|
|
42
|
-
const number = exponent ? scientific : detail ? formatTokens : compactNumber;
|
|
43
|
-
const approximate = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
|
|
44
|
-
const limit = context.windowKnown ? number(context.window) : '?';
|
|
45
|
-
return `${copy.context} ${approximate}${number(context.occupied)}/${limit} ${detail ? 'tokens' : 'tok'}`;
|
|
35
|
+
export function validContext(context) {
|
|
36
|
+
return context.windowKnown && context.window > 0 &&
|
|
37
|
+
[context.occupied, context.window, context.pct, context.deltaTokens].every(n => Number.isFinite(n) && n >= 0);
|
|
46
38
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
list.push(field('services', `${copy.services} ${input.servicesActive.live}`, 'warning'));
|
|
53
|
-
if (Number.isFinite(input.usageTotal.costUsd))
|
|
54
|
-
list.push(field('cost', `${copy.estimatedCost} ${formatCost(input.usageTotal.costUsd).slice(1)}`));
|
|
55
|
-
if (input.phase !== 'idle') {
|
|
56
|
-
if (input.tokPerSec > 0 && Number.isFinite(input.tokPerSec))
|
|
57
|
-
list.push(field('speed', `${copy.speed} ${input.tokPerSec} tok/s`));
|
|
58
|
-
if (Number.isFinite(input.elapsedMs))
|
|
59
|
-
list.push(field('elapsed', `${copy.elapsed} ${formatDuration(input.elapsedMs)}`));
|
|
60
|
-
}
|
|
61
|
-
if (input.thinkingLevel !== 'off')
|
|
62
|
-
list.push(field('thinking', `think:${cleanStatusText(input.thinkingLevel)}`));
|
|
63
|
-
list.push(field('usage', `${copy.sessionInput} ${compactNumber(promptTokensOf(input.usageTotal))} ${copy.output} ${compactNumber(input.usageTotal.outputTokens)} tok`));
|
|
64
|
-
if (input.context.windowKnown && Number.isFinite(input.context.pct))
|
|
65
|
-
list.push(field('percentage', `${input.context.source === 'estimate' || input.context.deltaTokens > 0 || !input.context.windowKnown ? '~' : ''}${Math.round(input.context.pct)}%`));
|
|
66
|
-
if (input.ecoRung)
|
|
67
|
-
list.push(field('eco', 'eco'));
|
|
68
|
-
if (input.fastActive)
|
|
69
|
-
list.push(field('fast', `fast${input.fastActive.inFlight ? '*' : ''}`));
|
|
70
|
-
if (input.teamActive)
|
|
71
|
-
list.push(field('team', `agents ${input.teamActive.running}/${input.teamActive.total}`));
|
|
72
|
-
if (input.todoActive)
|
|
73
|
-
list.push(field('todo', `todo ${input.todoActive.done}/${input.todoActive.total}`));
|
|
74
|
-
if (input.scrolledLines)
|
|
75
|
-
list.push(field('scroll', `^${input.scrolledLines}`, 'warning'));
|
|
76
|
-
return list;
|
|
39
|
+
function contextPercent(context) {
|
|
40
|
+
if (!validContext(context))
|
|
41
|
+
return '?';
|
|
42
|
+
const mark = context.source === 'estimate' || context.deltaTokens > 0 ? '~' : '';
|
|
43
|
+
return mark + (context.pct > 999 ? '>999' : Math.round(context.pct)) + '%';
|
|
77
44
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
45
|
+
export function shortDuration(ms) {
|
|
46
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
47
|
+
return '0s';
|
|
48
|
+
const seconds = Math.floor(ms / 1000);
|
|
49
|
+
if (seconds < 60)
|
|
50
|
+
return seconds + 's';
|
|
51
|
+
if (seconds < 3600)
|
|
52
|
+
return Math.floor(seconds / 60) + 'm' + String(seconds % 60).padStart(2, '0') + 's';
|
|
53
|
+
if (seconds < 86400)
|
|
54
|
+
return Math.floor(seconds / 3600) + 'h' + String(Math.floor(seconds / 60) % 60).padStart(2, '0') + 'm';
|
|
55
|
+
const days = Math.floor(seconds / 86400);
|
|
56
|
+
return days > 999 ? '>999d' : days + 'd';
|
|
57
|
+
}
|
|
58
|
+
function speedNumber(input) {
|
|
59
|
+
if (!input.speedKnown || input.phase === 'idle' || input.elapsedMs <= 500
|
|
60
|
+
|| !Number.isFinite(input.tokPerSec) || input.tokPerSec < 0)
|
|
61
|
+
return '--';
|
|
62
|
+
const value = input.tokPerSec;
|
|
63
|
+
if (value > 99e6)
|
|
64
|
+
return '>99M';
|
|
65
|
+
if (value >= 999950)
|
|
66
|
+
return (value / 1e6).toFixed(1) + 'M';
|
|
67
|
+
if (value > 999)
|
|
68
|
+
return (value / 1000).toFixed(1) + 'k';
|
|
69
|
+
return String(Math.round(value));
|
|
70
|
+
}
|
|
71
|
+
function feedbackLabel(input, form) {
|
|
72
|
+
const feedback = input.feedback;
|
|
73
|
+
if (!feedback)
|
|
74
|
+
return undefined;
|
|
75
|
+
const error = feedback.kind === 'copy-error' || feedback.kind === 'copy-cleanup';
|
|
76
|
+
if (error && input.escapeAction === 'force-stop') {
|
|
77
|
+
return form === 'full' ? 'Copy err / Esc force' : form === 'tiny' ? 'E/F' : 'Err/EscF';
|
|
100
78
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
field('context', contextText(input.context, true))];
|
|
104
|
-
for (const upgrade of upgrades) {
|
|
105
|
-
const index = fields.findIndex((f) => f.id === upgrade.id);
|
|
106
|
-
if (index >= 0 && width(fields) - fields[index].cells + upgrade.cells <= budget)
|
|
107
|
-
fields[index] = upgrade;
|
|
79
|
+
if (error && input.escapeAction === 'confirm-stop' && input.interruptPhase === 'armed') {
|
|
80
|
+
return form === 'full' ? 'Copy err / Esc stop' : form === 'tiny' ? 'E/S' : 'Err/EscS';
|
|
108
81
|
}
|
|
109
|
-
|
|
82
|
+
const labels = {
|
|
83
|
+
confirm: ['Awaiting confirmation', 'Ask', 'Ask'], stopping: ['Esc stop', 'EscS', 'EscS'],
|
|
84
|
+
'force-stop': ['Esc force', 'EscF', 'EscF'], exit: ['^C exit', '^Cexit', '^C'],
|
|
85
|
+
copying: ['Copying', 'Copy', 'Copy'], copied: ['Copied', 'Copied', 'Copy'],
|
|
86
|
+
'copy-sent': ['Copy sent', 'Sent', 'Sent'], 'copy-cleanup': ['Copy cleanup', 'Err', 'Err'],
|
|
87
|
+
'copy-error': ['Copy err', 'Err', 'Err'], notice: ['Notice', 'Notice', 'Note'],
|
|
88
|
+
};
|
|
89
|
+
return labels[feedback.kind][form === 'full' ? 0 : form === 'short' ? 1 : 2];
|
|
90
|
+
}
|
|
91
|
+
const thinkingCodes = { off: 'O', minimal: 'N', low: 'L', medium: 'M', high: 'H', xhigh: 'X' };
|
|
92
|
+
/** All five fields survive each width rung; only the redraw carrier is outside this plan. */
|
|
93
|
+
export function planPrimaryStatusFields(input) {
|
|
94
|
+
const budget = Math.max(0, (Number.isFinite(input.columns) ? Math.floor(input.columns) : 80) - 1);
|
|
95
|
+
const labels = phaseLabels(input);
|
|
96
|
+
const glyphText = cleanStatusText(input.spinner ?? '-');
|
|
97
|
+
const glyph = stringWidth(glyphText) === 1 ? glyphText : '-';
|
|
98
|
+
const code = thinkingCodes[input.thinkingLevel] ?? '?';
|
|
99
|
+
const pct = contextPercent(input.context);
|
|
100
|
+
const speed = speedNumber(input);
|
|
101
|
+
const tone = input.feedback?.level === 'error' || input.runOutcome === 'failed'
|
|
102
|
+
? 'error' : input.feedback?.level === 'warn' || input.waitingForConfirmation || input.stopping ? 'warning' : 'normal';
|
|
103
|
+
const build = (rung) => {
|
|
104
|
+
const full = rung === 0;
|
|
105
|
+
const label = feedbackLabel(input, full ? 'full' : rung >= 4 ? 'tiny' : 'short')
|
|
106
|
+
?? (full ? labels[1] : rung >= 4 ? labels[0].slice(0, 4) : labels[0]);
|
|
107
|
+
return statusLinePlan([
|
|
108
|
+
statusField('phase', glyph + (rung >= 5 ? '' : ' ') + label, tone),
|
|
109
|
+
statusField('context', (full ? 'Context ' : 'C') + pct),
|
|
110
|
+
statusField('thinking', full ? 'Think ' + (thinkingCodes[input.thinkingLevel] ? input.thinkingLevel : '?')
|
|
111
|
+
: (rung >= 2 ? '' : 'Th:') + code),
|
|
112
|
+
statusField('speed', speed + (full ? ' tok/s' : rung >= 3 ? '/s' : 't/s')),
|
|
113
|
+
statusField('elapsed', full && Number.isFinite(input.elapsedMs) && input.elapsedMs > 0
|
|
114
|
+
? formatDuration(input.elapsedMs) : shortDuration(input.elapsedMs)),
|
|
115
|
+
], full ? ' | ' : ' ');
|
|
116
|
+
};
|
|
117
|
+
let plan = build(0);
|
|
118
|
+
for (let rung = 1; plan.cells > budget && rung <= 5; rung++)
|
|
119
|
+
plan = build(rung);
|
|
120
|
+
return plan;
|
|
110
121
|
}
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Prefers spaces; hard-breaks any single token longer than `width` (a URL or a
|
|
20
20
|
* long path, which must not be dropped and cannot be wrapped politely).
|
|
21
|
+
* Width is measured in terminal cells and hard breaks preserve graphemes. A
|
|
22
|
+
* single grapheme wider than the limit stays intact on its own row.
|
|
21
23
|
* Existing newlines are honoured as paragraph breaks. Returns `[]` for empty
|
|
22
24
|
* input so callers can splice the result without an emptiness branch.
|
|
23
25
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { graphemes, graphemeWidth } from '../editor-navigation.js';
|
|
1
2
|
/**
|
|
2
3
|
* `wrapToRows` — the prose wrapper for `OverlayFrame`'s controlled (mode A)
|
|
3
4
|
* content (plan-mode §6.6).
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
*
|
|
19
20
|
* Prefers spaces; hard-breaks any single token longer than `width` (a URL or a
|
|
20
21
|
* long path, which must not be dropped and cannot be wrapped politely).
|
|
22
|
+
* Width is measured in terminal cells and hard breaks preserve graphemes. A
|
|
23
|
+
* single grapheme wider than the limit stays intact on its own row.
|
|
21
24
|
* Existing newlines are honoured as paragraph breaks. Returns `[]` for empty
|
|
22
25
|
* input so callers can splice the result without an emptiness branch.
|
|
23
26
|
*/
|
|
@@ -40,32 +43,43 @@ export function wrapToRows(text, width) {
|
|
|
40
43
|
function wrapParagraph(paragraph, limit) {
|
|
41
44
|
const rows = [];
|
|
42
45
|
let current = '';
|
|
46
|
+
let currentWidth = 0;
|
|
43
47
|
for (const word of paragraph.split(/\s+/).filter((w) => w.length > 0)) {
|
|
44
|
-
|
|
48
|
+
const parts = graphemes(word);
|
|
49
|
+
const wordWidth = parts.reduce((sum, part) => sum + graphemeWidth(part.text), 0);
|
|
50
|
+
if (wordWidth > limit) {
|
|
45
51
|
// Flush what we have, then chop the over-long token into full-width
|
|
46
52
|
// pieces. Its tail becomes the new `current` so the next word can still
|
|
47
53
|
// join it.
|
|
48
54
|
if (current.length > 0) {
|
|
49
55
|
rows.push(current);
|
|
50
56
|
current = '';
|
|
57
|
+
currentWidth = 0;
|
|
51
58
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
for (const part of parts) {
|
|
60
|
+
const cells = graphemeWidth(part.text);
|
|
61
|
+
if (current && currentWidth + cells > limit) {
|
|
62
|
+
rows.push(current);
|
|
63
|
+
current = '';
|
|
64
|
+
currentWidth = 0;
|
|
65
|
+
}
|
|
66
|
+
current += part.text;
|
|
67
|
+
currentWidth += cells;
|
|
56
68
|
}
|
|
57
|
-
current = rest;
|
|
58
69
|
continue;
|
|
59
70
|
}
|
|
60
71
|
if (current.length === 0) {
|
|
61
72
|
current = word;
|
|
73
|
+
currentWidth = wordWidth;
|
|
62
74
|
}
|
|
63
|
-
else if (
|
|
75
|
+
else if (currentWidth + 1 + wordWidth <= limit) {
|
|
64
76
|
current = `${current} ${word}`;
|
|
77
|
+
currentWidth += 1 + wordWidth;
|
|
65
78
|
}
|
|
66
79
|
else {
|
|
67
80
|
rows.push(current);
|
|
68
81
|
current = word;
|
|
82
|
+
currentWidth = wordWidth;
|
|
69
83
|
}
|
|
70
84
|
}
|
|
71
85
|
if (current.length > 0)
|
|
@@ -12,6 +12,7 @@ export interface ConfirmState {
|
|
|
12
12
|
resolve: (approved: boolean) => void;
|
|
13
13
|
}
|
|
14
14
|
interface ConfirmDialogProps {
|
|
15
|
+
isActive?: boolean;
|
|
15
16
|
state: ConfirmState;
|
|
16
17
|
maxRows: number;
|
|
17
18
|
cols: number;
|
|
@@ -26,5 +27,5 @@ interface ConfirmDialogProps {
|
|
|
26
27
|
* title and border match the other three overlays; the existing `useInput` is
|
|
27
28
|
* untouched.
|
|
28
29
|
*/
|
|
29
|
-
export declare function ConfirmDialog({ state, maxRows, cols, theme, caps, onClose, }: ConfirmDialogProps): React.ReactElement;
|
|
30
|
+
export declare function ConfirmDialog({ state, isActive, maxRows, cols, theme, caps, onClose, }: ConfirmDialogProps): React.ReactElement;
|
|
30
31
|
export {};
|
|
@@ -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
|
}
|