@itookit/dsht 0.3.8 → 0.5.1
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/README.i18n.yaml +2 -2
- package/README.md +30 -11
- package/README.zh.md +30 -11
- package/dist/cli/dsht.js +203 -18
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +295 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +60 -0
- package/dist/cli/verifier.js +242 -0
- package/dist/contracts.d.ts +344 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection.d.ts +11 -29
- package/dist/controller/connection.js +26 -60
- package/dist/controller/controller.d.ts +616 -166
- package/dist/controller/controller.js +1395 -146
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +126 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +73 -72
- package/dist/session/controller.js +185 -209
- package/dist/session/history.d.ts +6 -18
- package/dist/session/history.js +1 -24
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +25 -52
- package/dist/session/info.js +39 -25
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/telemetry.d.ts +12 -13
- package/dist/session/telemetry.js +27 -58
- package/dist/session/transcript.d.ts +0 -6
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +31 -1
- package/dist/shell/controller.js +34 -2
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/index.js +2 -2
- package/dist/shell/runner.d.ts +10 -0
- package/dist/shell/runner.js +48 -9
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +166 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/state.d.ts +14 -4
- package/dist/state.js +3 -2
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +856 -441
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +15 -2
- package/dist/ui/chat/shell-view.js +37 -3
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- package/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +1 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -104
- package/dist/ui/commands/parse.js +0 -135
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
package/dist/ui/chat/header.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Fixed conversation header: session title, workspace, and the agent-preset label. */
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import { toolLine } from "../../
|
|
4
|
+
import { toolLine } from "../../text.js";
|
|
5
5
|
import { Frozen } from "../frozen.js";
|
|
6
6
|
import { useTheme } from "../theme/index.js";
|
|
7
7
|
/** Session title and optional preset label above the divider.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HistoryRow } from '../../
|
|
1
|
+
import type { HistoryRow } from '../../contracts.ts';
|
|
2
2
|
/** Render a viewport with role, reasoning, tool and result colors from the selected theme.
|
|
3
3
|
*
|
|
4
4
|
* Rows are sibling text nodes rather than lines inside one text node: Ink measures and caches text per
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoopProgress } from '../../contracts.ts';
|
|
2
|
+
/** Progress line shown above the composer.
|
|
3
|
+
*
|
|
4
|
+
* It names no command: the protocol supplies `title`, and the remaining fields are the generic
|
|
5
|
+
* step/attempt score shape, so a new loop protocol renders here without a UI change.
|
|
6
|
+
* @param props - The application-produced loop snapshot.
|
|
7
|
+
* @returns One dim line, brightened while the loop is still running.
|
|
8
|
+
*/
|
|
9
|
+
export declare function LoopStatus({ progress }: {
|
|
10
|
+
progress: LoopProgress;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** One-line progress of a client-driven agent loop, for whichever protocol is running. */
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { useTheme } from "../theme/index.js";
|
|
5
|
+
/** Progress line shown above the composer.
|
|
6
|
+
*
|
|
7
|
+
* It names no command: the protocol supplies `title`, and the remaining fields are the generic
|
|
8
|
+
* step/attempt score shape, so a new loop protocol renders here without a UI change.
|
|
9
|
+
* @param props - The application-produced loop snapshot.
|
|
10
|
+
* @returns One dim line, brightened while the loop is still running.
|
|
11
|
+
*/
|
|
12
|
+
export function LoopStatus({ progress }) {
|
|
13
|
+
const theme = useTheme();
|
|
14
|
+
const running = progress.phase === 'running';
|
|
15
|
+
// A paused run is waiting for the operator, not finished: the line says what to do about it.
|
|
16
|
+
const paused = progress.active && progress.phase === 'needs-human';
|
|
17
|
+
// A run stopped on a host request shows what the host is waiting for, not just the phase name.
|
|
18
|
+
const interaction = progress.interaction === undefined ? '' : ` · ${progress.interaction.kind}: ${progress.interaction.text}`;
|
|
19
|
+
// A run a verifier ended early says why: the reason is the only part of that verdict a reader acts on.
|
|
20
|
+
const exit = progress.exit === undefined ? '' : ` · ${progress.exit.reason}`;
|
|
21
|
+
// `passed` only claims the rounds that ran, so the line says which ones it covered.
|
|
22
|
+
const scope = progress.phase === 'passed' ? ` · ${progress.scope}` : '';
|
|
23
|
+
// What the live run waits on is controller data, not a note: a work turn is already visible as the
|
|
24
|
+
// session working, so only the states without a host turn of their own are named here.
|
|
25
|
+
const activity = running && progress.activity !== undefined && progress.activity !== 'turn'
|
|
26
|
+
? ` · ${progress.activity}` : '';
|
|
27
|
+
return _jsxs(Text, { color: progress.active ? theme.colors.context : theme.colors.muted, children: [progress.title, progress.stepLabel === undefined ? '' : ` · ${progress.stepLabel}`, " \u00B7 step ", progress.step, "/", progress.to, " \u00B7 attempt ", progress.attempt, "/", progress.tries, " \u00B7 best ", progress.best, "/", progress.score, running ? '' : paused ? ' · needs you · /loop answer' : ` · ${progress.phase}`, activity, scope, exit, interaction, progress.note === undefined ? '' : ` · ${progress.note}`] });
|
|
28
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Presentation model for the workspace and session lists.
|
|
2
|
+
*
|
|
3
|
+
* Marker, label and rollup text are the list screens' vocabulary, so they live next to the screens
|
|
4
|
+
* that render them. `session/navigation.ts` keeps only target resolution, which the domain needs.
|
|
5
|
+
*/
|
|
6
|
+
import { type ObjectValue } from '../../json.ts';
|
|
7
|
+
/** User-visible activity of one session, most actionable first.
|
|
8
|
+
*
|
|
9
|
+
* `needs` is the only state that asks the user to do something, and it outranks the host's running
|
|
10
|
+
* flag because a turn waiting on an answer is running only in the mechanical sense. `blank` stays a
|
|
11
|
+
* marker for a session that never sent a turn, but it is not a status and never enters a rollup.
|
|
12
|
+
*/
|
|
13
|
+
export type SessionState = 'needs' | 'running' | 'idle' | 'blank';
|
|
14
|
+
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
15
|
+
* @param session - Session summary from `session/list`.
|
|
16
|
+
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
17
|
+
* @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sessionState(session: ObjectValue, pending?: boolean): SessionState;
|
|
20
|
+
/** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
|
|
21
|
+
export declare const SESSION_MARKERS: Record<SessionState, string>;
|
|
22
|
+
/** One word per state, so every screen names the same state the same way. */
|
|
23
|
+
export declare const STATE_LABELS: Record<SessionState, string>;
|
|
24
|
+
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
25
|
+
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
26
|
+
* @param now - Current epoch milliseconds.
|
|
27
|
+
* @returns `now`, minutes, hours or days.
|
|
28
|
+
*/
|
|
29
|
+
export declare function activityAge(time: number | undefined, now: number): string;
|
|
30
|
+
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
31
|
+
* @param session - Session summary from `session/list`.
|
|
32
|
+
* @param now - Current epoch milliseconds.
|
|
33
|
+
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
34
|
+
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sessionStatus(session: ObjectValue, now: number, pending?: boolean): string;
|
|
37
|
+
/** States a workspace rollup reports, most actionable first. */
|
|
38
|
+
export declare const ROLLUP_STATES: readonly ["needs", "running", "idle"];
|
|
39
|
+
/** One state a workspace rollup reports. */
|
|
40
|
+
export type RollupState = (typeof ROLLUP_STATES)[number];
|
|
41
|
+
/** One counted state of a workspace rollup. */
|
|
42
|
+
export interface RollupCount {
|
|
43
|
+
state: RollupState;
|
|
44
|
+
count: number;
|
|
45
|
+
}
|
|
46
|
+
/** How much room a rollup has for words. */
|
|
47
|
+
export type RollupStyle = 'words' | 'badges';
|
|
48
|
+
/** Count the sessions of one workspace by the state each reports.
|
|
49
|
+
*
|
|
50
|
+
* Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
|
|
51
|
+
* absence of activity, and listing it beside real work only makes the rollup harder to read.
|
|
52
|
+
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
53
|
+
* @param pending - Session IDs this client holds an unanswered interaction for.
|
|
54
|
+
* @returns One count per state that occurs, most actionable first, or an empty list.
|
|
55
|
+
*/
|
|
56
|
+
export declare function workspaceCounts(sessions: readonly ObjectValue[], pending?: ReadonlySet<string>): RollupCount[];
|
|
57
|
+
/** Render one rollup as separately coloured cells.
|
|
58
|
+
*
|
|
59
|
+
* Each cell after the first carries the separator that joins it to the previous one, so a caller can
|
|
60
|
+
* colour the cells independently without losing the text {@link workspaceStatus} would produce.
|
|
61
|
+
* @param counts - Counts from {@link workspaceCounts}.
|
|
62
|
+
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
63
|
+
* @returns The cells in the order given, with their separators.
|
|
64
|
+
*/
|
|
65
|
+
export declare function workspaceSegments(counts: readonly RollupCount[], style?: RollupStyle): {
|
|
66
|
+
state: RollupState;
|
|
67
|
+
text: string;
|
|
68
|
+
}[];
|
|
69
|
+
/** Render one rollup as plain text, the same way every screen and test reads it.
|
|
70
|
+
* @param counts - Counts from {@link workspaceCounts}.
|
|
71
|
+
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
72
|
+
* @returns The joined cell text, empty when nothing was counted.
|
|
73
|
+
*/
|
|
74
|
+
export declare function workspaceStatus(counts: readonly RollupCount[], style?: RollupStyle): string;
|
|
75
|
+
/** Marker key for the compact rollup, which has no room for the words. */
|
|
76
|
+
export declare const ROLLUP_LEGEND: string;
|
|
77
|
+
/** Secondary path text for one workspace row.
|
|
78
|
+
*
|
|
79
|
+
* The title is usually the last path segment, so repeating it wastes the row; the parent directory
|
|
80
|
+
* is what distinguishes two checkouts. A title that does not name the last segment keeps the full
|
|
81
|
+
* path, because dropping it would hide where the workspace actually lives.
|
|
82
|
+
* @param path - Registered host directory.
|
|
83
|
+
* @param title - Workspace title as the row already shows it.
|
|
84
|
+
* @returns The path to show beside the row, or an empty string when nothing is left.
|
|
85
|
+
*/
|
|
86
|
+
export declare function workspaceDetail(path: string, title: string): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Classify one session from the host's list summary; nothing is inferred from silence.
|
|
2
|
+
* @param session - Session summary from `session/list`.
|
|
3
|
+
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
4
|
+
* @returns Needs-you while an answer is owed, running while its agent works, otherwise idle or blank.
|
|
5
|
+
*/
|
|
6
|
+
export function sessionState(session, pending = false) {
|
|
7
|
+
if (pending)
|
|
8
|
+
return 'needs';
|
|
9
|
+
if (session.running === true)
|
|
10
|
+
return 'running';
|
|
11
|
+
return session.blank === true ? 'blank' : 'idle';
|
|
12
|
+
}
|
|
13
|
+
/** Leading marker per state: a question mark, a working clock, a filled dot, and an unused circle. */
|
|
14
|
+
export const SESSION_MARKERS = { needs: '?', running: '◐', idle: '●', blank: '○' };
|
|
15
|
+
/** One word per state, so every screen names the same state the same way. */
|
|
16
|
+
export const STATE_LABELS = { needs: 'needs you', running: 'working', idle: 'ready', blank: 'empty' };
|
|
17
|
+
/** Coarse age of a session's last activity, so the column stays steady between list refreshes.
|
|
18
|
+
* @param time - Epoch milliseconds of the last activity, when the summary reported one.
|
|
19
|
+
* @param now - Current epoch milliseconds.
|
|
20
|
+
* @returns `now`, minutes, hours or days.
|
|
21
|
+
*/
|
|
22
|
+
export function activityAge(time, now) {
|
|
23
|
+
if (time === undefined || !Number.isFinite(time))
|
|
24
|
+
return '';
|
|
25
|
+
const seconds = Math.max(0, Math.floor((now - time) / 1000));
|
|
26
|
+
if (seconds < 60)
|
|
27
|
+
return 'now';
|
|
28
|
+
const minutes = Math.floor(seconds / 60);
|
|
29
|
+
if (minutes < 60)
|
|
30
|
+
return `${minutes}m`;
|
|
31
|
+
const hours = Math.floor(minutes / 60);
|
|
32
|
+
return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
|
|
33
|
+
}
|
|
34
|
+
/** Status cell for one session row: its state marker and the age of its last activity.
|
|
35
|
+
* @param session - Session summary from `session/list`.
|
|
36
|
+
* @param now - Current epoch milliseconds.
|
|
37
|
+
* @param pending - Whether this client holds an unanswered interaction for that session.
|
|
38
|
+
* @returns Marker with an optional age, without a trailing space when unknown.
|
|
39
|
+
*/
|
|
40
|
+
export function sessionStatus(session, now, pending = false) {
|
|
41
|
+
const age = activityAge(typeof session.updatedAt === 'number' ? session.updatedAt : undefined, now);
|
|
42
|
+
return `${SESSION_MARKERS[sessionState(session, pending)]}${age === '' ? '' : ` ${age}`}`;
|
|
43
|
+
}
|
|
44
|
+
/** States a workspace rollup reports, most actionable first. */
|
|
45
|
+
export const ROLLUP_STATES = ['needs', 'running', 'idle'];
|
|
46
|
+
/** Count the sessions of one workspace by the state each reports.
|
|
47
|
+
*
|
|
48
|
+
* Blank sessions are counted by neither a badge nor a word: a session that never sent a turn is the
|
|
49
|
+
* absence of activity, and listing it beside real work only makes the rollup harder to read.
|
|
50
|
+
* @param sessions - Sessions whose `sessionIds` belong to the workspace.
|
|
51
|
+
* @param pending - Session IDs this client holds an unanswered interaction for.
|
|
52
|
+
* @returns One count per state that occurs, most actionable first, or an empty list.
|
|
53
|
+
*/
|
|
54
|
+
export function workspaceCounts(sessions, pending = new Set()) {
|
|
55
|
+
const counts = { needs: 0, running: 0, idle: 0 };
|
|
56
|
+
for (const session of sessions) {
|
|
57
|
+
const id = session.sessionId;
|
|
58
|
+
const state = sessionState(session, typeof id === 'string' && pending.has(id));
|
|
59
|
+
if (state !== 'blank')
|
|
60
|
+
counts[state] += 1;
|
|
61
|
+
}
|
|
62
|
+
return ROLLUP_STATES.filter(state => counts[state] > 0).map(state => ({ state, count: counts[state] }));
|
|
63
|
+
}
|
|
64
|
+
/** Render one rollup as separately coloured cells.
|
|
65
|
+
*
|
|
66
|
+
* Each cell after the first carries the separator that joins it to the previous one, so a caller can
|
|
67
|
+
* colour the cells independently without losing the text {@link workspaceStatus} would produce.
|
|
68
|
+
* @param counts - Counts from {@link workspaceCounts}.
|
|
69
|
+
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
70
|
+
* @returns The cells in the order given, with their separators.
|
|
71
|
+
*/
|
|
72
|
+
export function workspaceSegments(counts, style = 'words') {
|
|
73
|
+
const separator = style === 'words' ? ' · ' : ' ';
|
|
74
|
+
return counts.map(({ state, count }, index) => ({ state,
|
|
75
|
+
text: `${index === 0 ? '' : separator}${style === 'words' ? `${SESSION_MARKERS[state]} ${count} ${STATE_LABELS[state]}` : `${SESSION_MARKERS[state]}${count}`}` }));
|
|
76
|
+
}
|
|
77
|
+
/** Render one rollup as plain text, the same way every screen and test reads it.
|
|
78
|
+
* @param counts - Counts from {@link workspaceCounts}.
|
|
79
|
+
* @param style - `words` spells each state out; `badges` keeps only the marker and the count.
|
|
80
|
+
* @returns The joined cell text, empty when nothing was counted.
|
|
81
|
+
*/
|
|
82
|
+
export function workspaceStatus(counts, style = 'words') {
|
|
83
|
+
return workspaceSegments(counts, style).map(segment => segment.text).join('');
|
|
84
|
+
}
|
|
85
|
+
/** Marker key for the compact rollup, which has no room for the words. */
|
|
86
|
+
export const ROLLUP_LEGEND = `${SESSION_MARKERS.idle} ${STATE_LABELS.idle} · ${SESSION_MARKERS.running} ${STATE_LABELS.running} · ${SESSION_MARKERS.needs} ${STATE_LABELS.needs}`;
|
|
87
|
+
/** Secondary path text for one workspace row.
|
|
88
|
+
*
|
|
89
|
+
* The title is usually the last path segment, so repeating it wastes the row; the parent directory
|
|
90
|
+
* is what distinguishes two checkouts. A title that does not name the last segment keeps the full
|
|
91
|
+
* path, because dropping it would hide where the workspace actually lives.
|
|
92
|
+
* @param path - Registered host directory.
|
|
93
|
+
* @param title - Workspace title as the row already shows it.
|
|
94
|
+
* @returns The path to show beside the row, or an empty string when nothing is left.
|
|
95
|
+
*/
|
|
96
|
+
export function workspaceDetail(path, title) {
|
|
97
|
+
if (!path)
|
|
98
|
+
return '';
|
|
99
|
+
const trimmed = path.replace(/\/+$/u, '');
|
|
100
|
+
const cut = trimmed.lastIndexOf('/');
|
|
101
|
+
const base = cut < 0 ? trimmed : trimmed.slice(cut + 1);
|
|
102
|
+
if (base !== title)
|
|
103
|
+
return path;
|
|
104
|
+
if (cut > 0)
|
|
105
|
+
return trimmed.slice(0, cut);
|
|
106
|
+
return cut === 0 ? '/' : '';
|
|
107
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { ShellBlock } from '../../
|
|
1
|
+
import type { HistoryRow, RowKind } from '../../contracts.ts';
|
|
2
|
+
import type { ShellBlock } from '../../contracts.ts';
|
|
3
3
|
/** The part of a transcript layout this module needs to place a block. */
|
|
4
4
|
export interface RowSource {
|
|
5
5
|
/** Number of host rows, live tail included. */
|
|
@@ -30,5 +30,18 @@ export declare function blockRows(run: ShellBlock, width: number): HistoryRow[];
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function mergeShellRuns(layout: RowSource, runs: readonly ShellBlock[], width: number): {
|
|
32
32
|
total: number;
|
|
33
|
+
/** Merged row index of each block bar that opens a readable source, to its source id. */
|
|
34
|
+
sources: ReadonlyMap<number, string>;
|
|
33
35
|
viewport(start: number, end: number): HistoryRow[];
|
|
34
36
|
};
|
|
37
|
+
/** Wrap plain local text into terminal rows, preserving its own whitespace.
|
|
38
|
+
*
|
|
39
|
+
* Command output is aligned by spaces and indented by stack traces, so this never collapses runs of
|
|
40
|
+
* whitespace the way tool summaries do; only the terminal width decides where a row breaks.
|
|
41
|
+
* @param text - Raw text, possibly containing newlines.
|
|
42
|
+
* @param width - Available terminal columns.
|
|
43
|
+
* @param kind - Row kind used for coloring.
|
|
44
|
+
* @param highlight - Whether the rows are a local command line drawn on the command bar.
|
|
45
|
+
* @returns One row per wrapped terminal line; empty text yields one empty row.
|
|
46
|
+
*/
|
|
47
|
+
export declare function plainRows(text: string, width: number, kind: RowKind, highlight?: boolean): HistoryRow[];
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the bottom of the screen.
|
|
6
6
|
*/
|
|
7
7
|
import stringWidth from 'string-width';
|
|
8
|
-
import
|
|
8
|
+
import wrapAnsi from 'wrap-ansi';
|
|
9
9
|
/** First output row's marker, so a block reads as the command's result. */
|
|
10
10
|
const MARKER = ' ⎿ ';
|
|
11
11
|
/** Rows after the first, aligned under the marker. */
|
|
@@ -17,7 +17,16 @@ const GUTTER = ' ';
|
|
|
17
17
|
*/
|
|
18
18
|
export function blockRows(run, width) {
|
|
19
19
|
const reserve = Math.max(stringWidth(MARKER), stringWidth(GUTTER));
|
|
20
|
-
const rows = [{ text:
|
|
20
|
+
const rows = [{ text: run.kind === 'note' ? run.command : `! ${run.command}`,
|
|
21
|
+
kind: 'shell', bold: true, highlight: true }];
|
|
22
|
+
// A note produced nothing to show, so its second row is the action itself. The label is the whole
|
|
23
|
+
// affordance — a sentence saying "click this bar" left the reader clicking a row that was not the
|
|
24
|
+
// bar — and every row of a note opens the source (see `mergeShellRuns`).
|
|
25
|
+
if (run.kind === 'note') {
|
|
26
|
+
rows.push({ text: run.source === undefined ? `${MARKER}no readable session yet` : `${MARKER}view`,
|
|
27
|
+
kind: 'shell', bold: true });
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
21
30
|
const body = [
|
|
22
31
|
...(run.dropped > 0 ? [`… ${run.dropped} earlier lines dropped …`] : []),
|
|
23
32
|
...run.lines,
|
|
@@ -67,10 +76,12 @@ function rowAfter(layout, anchor) {
|
|
|
67
76
|
*/
|
|
68
77
|
export function mergeShellRuns(layout, runs, width) {
|
|
69
78
|
const placements = [];
|
|
79
|
+
const sources = new Map();
|
|
70
80
|
let cursor = 0;
|
|
71
81
|
for (const run of runs) {
|
|
72
82
|
const at = Math.max(cursor, Math.min(layout.length, rowAfter(layout, run.anchor)));
|
|
73
|
-
placements.push({ at, rows: blockRows(run, width) })
|
|
83
|
+
placements.push({ at, rows: blockRows(run, width), ...(run.source === undefined ? {} : { source: run.source }),
|
|
84
|
+
...(run.kind === 'note' ? { whole: true } : {}) });
|
|
74
85
|
cursor = at;
|
|
75
86
|
}
|
|
76
87
|
// Segments alternate host rows and block rows in merged index order.
|
|
@@ -82,6 +93,13 @@ export function mergeShellRuns(layout, runs, width) {
|
|
|
82
93
|
merged += placement.at - host;
|
|
83
94
|
host = placement.at;
|
|
84
95
|
}
|
|
96
|
+
// Only a `!` block's first row is its bar; the rows under it are its output, not its identity.
|
|
97
|
+
// A note has no output, so all of its rows open what it points at.
|
|
98
|
+
if (placement.source !== undefined) {
|
|
99
|
+
const opening = placement.whole === true ? placement.rows.length : 1;
|
|
100
|
+
for (let offset = 0; offset < opening; offset += 1)
|
|
101
|
+
sources.set(merged + offset, placement.source);
|
|
102
|
+
}
|
|
85
103
|
segments.push({ from: merged, count: placement.rows.length, rows: placement.rows });
|
|
86
104
|
merged += placement.rows.length;
|
|
87
105
|
}
|
|
@@ -90,6 +108,7 @@ export function mergeShellRuns(layout, runs, width) {
|
|
|
90
108
|
const total = merged + Math.max(0, layout.length - host);
|
|
91
109
|
return {
|
|
92
110
|
total,
|
|
111
|
+
sources,
|
|
93
112
|
viewport(start, end) {
|
|
94
113
|
const rows = [];
|
|
95
114
|
for (const segment of segments) {
|
|
@@ -109,3 +128,18 @@ export function mergeShellRuns(layout, runs, width) {
|
|
|
109
128
|
},
|
|
110
129
|
};
|
|
111
130
|
}
|
|
131
|
+
/** Wrap plain local text into terminal rows, preserving its own whitespace.
|
|
132
|
+
*
|
|
133
|
+
* Command output is aligned by spaces and indented by stack traces, so this never collapses runs of
|
|
134
|
+
* whitespace the way tool summaries do; only the terminal width decides where a row breaks.
|
|
135
|
+
* @param text - Raw text, possibly containing newlines.
|
|
136
|
+
* @param width - Available terminal columns.
|
|
137
|
+
* @param kind - Row kind used for coloring.
|
|
138
|
+
* @param highlight - Whether the rows are a local command line drawn on the command bar.
|
|
139
|
+
* @returns One row per wrapped terminal line; empty text yields one empty row.
|
|
140
|
+
*/
|
|
141
|
+
export function plainRows(text, width, kind, highlight = false) {
|
|
142
|
+
const columns = Math.max(1, width);
|
|
143
|
+
const wrapped = wrapAnsi(text === '' ? ' ' : text, columns, { hard: true, trim: false });
|
|
144
|
+
return wrapped.split('\n').map(line => ({ text: line, kind, ...(highlight ? { highlight: true } : {}) }));
|
|
145
|
+
}
|
package/dist/ui/chat/status.d.ts
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { ClientActivity, Coverage, LivePhase } from '../../contracts.ts';
|
|
2
|
+
/** One formatted cost scope, with the raw totals the money column needs. */
|
|
3
|
+
export interface StatusCostLine {
|
|
4
|
+
text: string;
|
|
5
|
+
amount: number;
|
|
6
|
+
unknown: number;
|
|
7
|
+
}
|
|
8
|
+
/** Everything the status bar renders, as plain data the composition root assembles. */
|
|
9
|
+
export interface StatusSource {
|
|
10
|
+
/** Host origin, so a reader can tell which deployment the numbers describe. */
|
|
11
|
+
host: string;
|
|
12
|
+
online: boolean;
|
|
13
|
+
status: string;
|
|
14
|
+
running: boolean;
|
|
15
|
+
/** What the client is working on now, merged by the controller: a host turn or a running loop.
|
|
16
|
+
*
|
|
17
|
+
* The bar renders this and never merges sources itself, so "busy", its clock and the loop's
|
|
18
|
+
* sub-state all come from one controller-owned answer.
|
|
19
|
+
*/
|
|
20
|
+
activity?: ClientActivity;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
sessionMode?: string;
|
|
23
|
+
workspaceLabel: string;
|
|
24
|
+
activeTurnStartedAt?: number;
|
|
25
|
+
pendingCount: number;
|
|
26
|
+
/** What the live attempt is doing now, straight from the record. */
|
|
27
|
+
livePhase?: LivePhase;
|
|
28
|
+
/** Retained host projections for the selected session. */
|
|
29
|
+
values: ObjectValue;
|
|
30
|
+
queued?: number;
|
|
31
|
+
jobs?: number;
|
|
32
|
+
defaultModel?: ObjectValue;
|
|
33
|
+
/** Billing summary; absent when this run has no ledger. */
|
|
34
|
+
cost?: {
|
|
35
|
+
sessionText: string;
|
|
36
|
+
/** Read on every render, so a day rollover reaches the bar without an application re-render. */
|
|
37
|
+
today(): StatusCostLine;
|
|
38
|
+
session(): StatusCostLine | undefined;
|
|
39
|
+
coverage: Coverage;
|
|
40
|
+
error?: string;
|
|
41
|
+
};
|
|
42
|
+
controlError?: string;
|
|
43
|
+
presetError?: string;
|
|
44
|
+
modelError?: string;
|
|
45
|
+
}
|
|
46
|
+
import type { ObjectValue } from '../../json.ts';
|
|
3
47
|
/** Format elapsed wall time, clamping clock skew instead of displaying negative durations.
|
|
4
48
|
* @param milliseconds - Elapsed duration.
|
|
5
49
|
* @returns Minute/second display, with hours when needed.
|
|
@@ -77,7 +121,7 @@ export declare function phaseText(milliseconds: number): string;
|
|
|
77
121
|
export declare function compactStatusRows(groups: StatusGroups, width: number): StatusSegment[][];
|
|
78
122
|
/** Render a live clock and selected-session metadata; the timer belongs to this mounted bar. */
|
|
79
123
|
export declare const StatusBar: import("react").NamedExoticComponent<{
|
|
80
|
-
|
|
124
|
+
source: StatusSource;
|
|
81
125
|
expanded?: boolean;
|
|
82
126
|
width?: number;
|
|
83
127
|
revision?: number;
|
package/dist/ui/chat/status.js
CHANGED
|
@@ -5,9 +5,8 @@ import { memo, useEffect, useState } from 'react';
|
|
|
5
5
|
import { Box, Text, useStdout } from 'ink';
|
|
6
6
|
import wrapAnsi from 'wrap-ansi';
|
|
7
7
|
import stringWidth from 'string-width';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { safeText } from "../../transport/wire.js";
|
|
8
|
+
import { toolLine } from "../../text.js";
|
|
9
|
+
import { safeText } from "../../text.js";
|
|
11
10
|
/** Format elapsed wall time, clamping clock skew instead of displaying negative durations.
|
|
12
11
|
* @param milliseconds - Elapsed duration.
|
|
13
12
|
* @returns Minute/second display, with hours when needed.
|
|
@@ -215,32 +214,33 @@ function measure(row) {
|
|
|
215
214
|
return row.reduce((sum, segment) => sum + stringWidth(segment.text), 0);
|
|
216
215
|
}
|
|
217
216
|
/** Render a live clock and selected-session metadata; the timer belongs to this mounted bar. */
|
|
218
|
-
export const StatusBar = memo(function StatusBar({
|
|
217
|
+
export const StatusBar = memo(function StatusBar({ source, expanded = false, width, scroll = 0, pageSize, onScroll, onOverflow, onRows, pauseReason }) {
|
|
219
218
|
const paused = pauseReason !== undefined;
|
|
220
219
|
const theme = useTheme();
|
|
221
220
|
const { stdout } = useStdout();
|
|
222
221
|
const [now, setNow] = useState(Date.now);
|
|
223
222
|
const [reported, setReported] = useState(1);
|
|
224
|
-
const running =
|
|
225
|
-
const
|
|
223
|
+
const running = source.running;
|
|
224
|
+
const activity = source.activity;
|
|
225
|
+
// The controller already merged a turn and a loop into one answer; the bar only draws it.
|
|
226
|
+
const busy = activity !== undefined;
|
|
227
|
+
const since = activity?.kind === 'turn' ? activity.since : activity?.kind === 'loop' ? activity.startedAt : undefined;
|
|
226
228
|
useEffect(() => {
|
|
227
229
|
setNow(Date.now());
|
|
228
230
|
if (paused)
|
|
229
231
|
return;
|
|
230
232
|
// An idle bar still re-reads the clock, so a day rollover reaches the cost it reports without
|
|
231
|
-
// waiting for an unrelated render; a
|
|
232
|
-
const timer = setInterval(() => setNow(Date.now()),
|
|
233
|
+
// waiting for an unrelated render; a busy bar keeps its per-second clock.
|
|
234
|
+
const timer = setInterval(() => setNow(Date.now()), busy ? 1000 : 60_000);
|
|
233
235
|
return () => clearInterval(timer);
|
|
234
|
-
}, [
|
|
235
|
-
const state =
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const sessionCost = controller.sessionCostText;
|
|
240
|
-
const todayCost = costs ? costText(costs.today()) : '?';
|
|
236
|
+
}, [busy, since, paused]);
|
|
237
|
+
const state = source;
|
|
238
|
+
const view = { values: source.values, queued: source.queued, jobs: source.jobs };
|
|
239
|
+
const costs = source.cost;
|
|
240
|
+
const sessionCost = source.cost?.sessionText ?? '?';
|
|
241
241
|
// `*` belongs to costText alone; incomplete coverage is a separate degradation, reported by `!`.
|
|
242
|
-
const coverage =
|
|
243
|
-
const label =
|
|
242
|
+
const coverage = source.cost?.coverage ?? 'complete';
|
|
243
|
+
const label = source.workspaceLabel;
|
|
244
244
|
if (!expanded) {
|
|
245
245
|
const selection = record(view.values.modelSelection);
|
|
246
246
|
const route = record(running ? selection.lastUsed ?? selection.next ?? state.defaultModel : selection.next ?? selection.lastUsed ?? state.defaultModel);
|
|
@@ -258,29 +258,38 @@ export const StatusBar = memo(function StatusBar({ controller, expanded = false,
|
|
|
258
258
|
const billed = buckets[0] === undefined || buckets[2] === undefined ? undefined : buckets[0] + buckets[2] + (buckets[3] ?? 0);
|
|
259
259
|
const hit = cacheHitText(buckets[2], billed);
|
|
260
260
|
const compactCount = (value) => value === undefined ? '?' : compactNumber.format(value);
|
|
261
|
-
const phase =
|
|
262
|
-
const clock =
|
|
261
|
+
const phase = source.livePhase;
|
|
262
|
+
const clock = busy && since !== undefined ? ` ${clockText(now - since)}` : '';
|
|
263
263
|
const phaseLabel = phase === undefined ? undefined
|
|
264
264
|
: phase.kind === 'tool' ? `${phase.name ?? 'tool'} ${phaseText(now - phase.startedAt)}`
|
|
265
265
|
: `${phase.kind === 'thinking' ? 'think' : 'write'} ${phaseText(now - phase.startedAt)}`;
|
|
266
|
+
// A host turn's phase wins; without one, the controller's loop activity names what the client is
|
|
267
|
+
// doing, so the bar never reports an idle session while a review is in flight.
|
|
268
|
+
const phaseSegment = !busy ? undefined
|
|
269
|
+
: phaseLabel !== undefined ? { text: phaseLabel }
|
|
270
|
+
: activity?.kind === 'loop' ? { text: `${activity.activity} ${activity.step}/${activity.total}` }
|
|
271
|
+
: activity?.kind === 'paused' ? { text: 'needs you' }
|
|
272
|
+
: undefined;
|
|
266
273
|
// The state token reports a fact and never guesses: a paused clock is named, offline and errors
|
|
267
274
|
// take the token over, and an unknown phase simply leaves the phase group empty. An answer this
|
|
268
275
|
// client still owes outranks the paused reason, because that reason is only why the clock stopped.
|
|
269
|
-
const stateToken = !
|
|
276
|
+
const stateToken = !source.online
|
|
270
277
|
? { text: '! Offline', color: theme.status.offline }
|
|
271
|
-
:
|
|
278
|
+
: source.controlError || source.modelError
|
|
272
279
|
? { text: '⚠ Error', color: theme.status.warning }
|
|
273
|
-
:
|
|
280
|
+
: source.pendingCount > 0
|
|
274
281
|
? { text: '? Needs you', color: theme.status.critical }
|
|
275
282
|
: pauseReason !== undefined
|
|
276
283
|
? { text: `⏸ ${pauseReason}${clock}`, color: theme.colors.muted }
|
|
277
|
-
:
|
|
284
|
+
: activity?.kind === 'paused' ? { text: '⏸ needs you', color: theme.status.critical }
|
|
285
|
+
: busy ? { text: `◐${clock}`, color: theme.status.working } : { text: '● Ready', color: theme.status.ready };
|
|
278
286
|
// One marker covers both scopes, because either an unpriceable record or a scan that has not
|
|
279
287
|
// covered every session makes the pair inexact as a reading.
|
|
280
288
|
const inexact = coverage !== 'complete';
|
|
281
289
|
const money = (session, today) => `¥: ${session === undefined ? '?' : session.amount.toFixed(2)}(${today.amount.toFixed(2)})${session?.unknown || today.unknown || inexact ? '*' : ''}`;
|
|
290
|
+
// Read here rather than from a snapshot, so the bar's own clock drives a day rollover.
|
|
282
291
|
const todayTotal = costs?.today();
|
|
283
|
-
const sessionTotal = costs
|
|
292
|
+
const sessionTotal = costs?.session();
|
|
284
293
|
const contextColor = percent === undefined ? theme.status.usage
|
|
285
294
|
: percent >= 95 ? theme.status.critical : percent >= 80 ? theme.status.warning : theme.status.context;
|
|
286
295
|
// Ten cells resolve context to tenths, the same resolution the percentage beside them reports.
|
|
@@ -288,10 +297,11 @@ export const StatusBar = memo(function StatusBar({ controller, expanded = false,
|
|
|
288
297
|
const groups = {
|
|
289
298
|
state: stateToken,
|
|
290
299
|
// A paused bar keeps the phase: the reason already says why the clock stopped, and dropping the
|
|
291
|
-
// running tool would leave the one question this bar exists to answer unanswered. A
|
|
292
|
-
// has none: the transcript keeps the last event it saw, and only the host knows
|
|
293
|
-
|
|
294
|
-
...(
|
|
300
|
+
// running tool would leave the one question this bar exists to answer unanswered. A bar with no
|
|
301
|
+
// work of its own has none: the transcript keeps the last event it saw, and only the host knows
|
|
302
|
+
// the turn ended.
|
|
303
|
+
...(phaseSegment === undefined ? {} : { phase: phaseSegment }),
|
|
304
|
+
...(busy && pauseReason === undefined ? { stop: { text: '^C' } } : {}),
|
|
295
305
|
...(todayTotal === undefined ? {} : { cost: { text: money(sessionTotal, todayTotal), color: theme.status.cost } }),
|
|
296
306
|
...(percent === undefined ? {} : {
|
|
297
307
|
context: { text: `ctx ${percent}%`, color: contextColor },
|
|
@@ -310,26 +320,27 @@ export const StatusBar = memo(function StatusBar({ controller, expanded = false,
|
|
|
310
320
|
}
|
|
311
321
|
return _jsx(Box, { flexDirection: "column", children: rows.map((row, index) => _jsx(Text, { wrap: "truncate-end", children: row.map((segment, position) => _jsx(Text, { color: segment.color ?? theme.colors.muted, bold: index === 0 && position === 0, children: segment.text }, position)) }, index)) });
|
|
312
322
|
}
|
|
313
|
-
return _jsx(StatusDetails, {
|
|
323
|
+
return _jsx(StatusDetails, { source: source, theme: theme, width: width, now: now, scroll: scroll, pageSize: pageSize, onScroll: onScroll, onOverflow: onOverflow });
|
|
314
324
|
});
|
|
315
325
|
/** Expanded detail panel.
|
|
316
326
|
*
|
|
317
327
|
* It is a separate component because it is the only branch that scrolls, and a hook behind the
|
|
318
328
|
* collapsed branch's early return would change the hook order between the two states.
|
|
319
329
|
*/
|
|
320
|
-
const StatusDetails = memo(function StatusDetails({
|
|
330
|
+
const StatusDetails = memo(function StatusDetails({ source, theme, width, now, scroll, pageSize, onScroll, onOverflow }) {
|
|
321
331
|
const { stdout } = useStdout();
|
|
322
|
-
const state =
|
|
323
|
-
const running =
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const
|
|
332
|
+
const state = source;
|
|
333
|
+
const running = source.running;
|
|
334
|
+
const activity = source.activity;
|
|
335
|
+
const busy = activity !== undefined;
|
|
336
|
+
const since = activity?.kind === 'turn' ? activity.since : activity?.kind === 'loop' ? activity.startedAt : undefined;
|
|
337
|
+
const view = { values: source.values, queued: source.queued, jobs: source.jobs };
|
|
338
|
+
const costs = source.cost;
|
|
339
|
+
const sessionCost = source.cost?.sessionText ?? '?';
|
|
340
|
+
const todayCost = source.cost?.today().text ?? '?';
|
|
330
341
|
// `*` belongs to costText alone; incomplete coverage is a separate degradation, reported by `!`.
|
|
331
|
-
const coverage =
|
|
332
|
-
const label =
|
|
342
|
+
const coverage = source.cost?.coverage ?? 'complete';
|
|
343
|
+
const label = source.workspaceLabel;
|
|
333
344
|
// Every detail row wraps to the terminal width, so a narrow terminal loses nothing; lines that
|
|
334
345
|
// still do not fit are scrolled rather than dropped, because the panel shares the screen height.
|
|
335
346
|
// Rows are merged and labelled compactly so a normal terminal shows every detail on one screen;
|
|
@@ -337,23 +348,27 @@ const StatusDetails = memo(function StatusDetails({ controller, theme, width, no
|
|
|
337
348
|
const turns = count(numeric(record(view.values.sessionStats).turns));
|
|
338
349
|
const duration = since === undefined ? 'unknown duration' : elapsedTime(now - since);
|
|
339
350
|
const detail = [
|
|
340
|
-
{ key: 'activity', color:
|
|
351
|
+
{ key: 'activity', color: source.pendingCount > 0 ? theme.status.critical : busy ? theme.colors.context : theme.colors.muted, text: source.pendingCount > 0
|
|
341
352
|
? '? Needs you · answer the request above to continue'
|
|
342
353
|
: running
|
|
343
|
-
? `◐ Working · ${duration}${
|
|
344
|
-
:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
354
|
+
? `◐ Working · ${duration}${source.activeTurnStartedAt === undefined ? ' (observed)' : ''} · Ctrl+C Stop`
|
|
355
|
+
: activity?.kind === 'loop'
|
|
356
|
+
? `◐ ${activity.activity} ${activity.step}/${activity.total} · ${duration} · Ctrl+C Stop`
|
|
357
|
+
: activity?.kind === 'paused'
|
|
358
|
+
? `⏸ ${activity.title} · needs you · /loop answer <text> or /loop abort`
|
|
359
|
+
: '● Ready · Ctrl+C exit' },
|
|
360
|
+
{ key: 'host', text: `${safeText(source.host)} · ${safeText(state.status)}${!source.online ? ' · offline, last known status' : ''}` },
|
|
361
|
+
...source.sessionId
|
|
362
|
+
? [{ key: 'session', text: `Session ${safeText(source.sessionId)}${source.sessionMode ? ` · ${safeText(source.sessionMode)}` : ''}` }] : [],
|
|
348
363
|
{ key: 'workspace', text: `Workspace ${safeText(label)}` },
|
|
349
|
-
...metricLines(view.values,
|
|
364
|
+
...metricLines(view.values, source.defaultModel, running).map((line, index) => ({ key: `metric-${index}`, text: safeText(line), dim: true })),
|
|
350
365
|
...costs ? [{ key: 'cost', text: `Cost ${sessionCost} session · ${todayCost} today · ${turns} turns`, dim: true }] : [],
|
|
351
366
|
{ key: 'queued', text: `Queued ${count(view.queued)} · Jobs ${count(view.jobs)}${costs ? '' : ` · ${turns} turns`}`, dim: true },
|
|
352
367
|
...costs && coverage === 'partial'
|
|
353
368
|
? [{ key: 'coverage', color: theme.colors.context, text: `Cost coverage incomplete: ${costs.error ? safeText(costs.error) : 'no complete scan yet'}` }] : [],
|
|
354
|
-
...
|
|
355
|
-
...
|
|
356
|
-
...
|
|
369
|
+
...source.controlError ? [{ key: 'control-error', color: theme.colors.context, text: safeText(source.controlError) }] : [],
|
|
370
|
+
...source.presetError ? [{ key: 'preset-error', color: theme.colors.context, text: `Preset names unavailable: ${safeText(source.presetError)}` }] : [],
|
|
371
|
+
...source.modelError ? [{ key: 'model-error', color: theme.colors.context, text: `Model catalog unavailable: ${safeText(source.modelError)}` }] : [],
|
|
357
372
|
];
|
|
358
373
|
// Border and horizontal padding take four columns, so a detail row wraps inside what is left.
|
|
359
374
|
const inner = Math.max(1, (width ?? Math.max(3, (stdout.columns ?? 80) - 2)) - 4);
|