@aiwayds/dsh-tui-pi 0.1.1 → 0.4.0
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.md +175 -41
- package/lib/agent-manager.d.ts +125 -0
- package/lib/agent-manager.js +298 -0
- package/lib/agent-manager.js.map +1 -0
- package/lib/agents.d.ts +30 -0
- package/lib/agents.js +489 -0
- package/lib/agents.js.map +1 -0
- package/lib/append-system.d.ts +2 -0
- package/lib/append-system.js +1 -1
- package/lib/append-system.js.map +1 -1
- package/lib/dsh-events.d.ts +12 -0
- package/lib/editor.d.ts +58 -0
- package/lib/editor.js +92 -0
- package/lib/editor.js.map +1 -1
- package/lib/footer.d.ts +46 -0
- package/lib/footer.js +65 -0
- package/lib/footer.js.map +1 -1
- package/lib/hotkeys.d.ts +93 -0
- package/lib/hotkeys.js +305 -0
- package/lib/hotkeys.js.map +1 -0
- package/lib/index.js +342 -61
- package/lib/index.js.map +1 -1
- package/lib/keymap.d.ts +243 -0
- package/lib/keymap.js +245 -0
- package/lib/keymap.js.map +1 -0
- package/lib/live-widgets.d.ts +59 -49
- package/lib/live-widgets.js +174 -117
- package/lib/live-widgets.js.map +1 -1
- package/lib/panels.d.ts +177 -0
- package/lib/panels.js +331 -0
- package/lib/panels.js.map +1 -0
- package/lib/selectors.d.ts +12 -1
- package/lib/selectors.js +1 -1
- package/lib/selectors.js.map +1 -1
- package/lib/session.d.ts +25 -0
- package/lib/session.js +173 -12
- package/lib/session.js.map +1 -1
- package/lib/settings.d.ts +1 -2
- package/lib/subagent-policy.d.ts +88 -0
- package/lib/subagent-policy.js +189 -0
- package/lib/subagent-policy.js.map +1 -0
- package/lib/subagent-viewer.d.ts +49 -0
- package/lib/subagent-viewer.js +523 -0
- package/lib/subagent-viewer.js.map +1 -0
- package/lib/theme/index.js +4 -0
- package/lib/theme/index.js.map +1 -1
- package/lib/theme/palette.d.ts +21 -3
- package/lib/theme/palette.js +27 -6
- package/lib/theme/palette.js.map +1 -1
- package/lib/theme-settings.d.ts +71 -9
- package/lib/theme-settings.js +145 -21
- package/lib/theme-settings.js.map +1 -1
- package/lib/tui.d.ts +23 -9
- package/lib/tui.js +110 -7
- package/lib/tui.js.map +1 -1
- package/package.json +3 -4
- package/patches/@earendil-works__pi-tui.patch +206 -2
- package/scripts/dev-install.mjs +87 -0
- package/scripts/link-dsh-closure.mjs +85 -0
- package/templates/agents/ArtyDuck.md +11 -0
- package/templates/agents/oldfox.md +33 -0
- package/templates/agents/workhorse.md +34 -0
package/lib/live-widgets.d.ts
CHANGED
|
@@ -9,13 +9,12 @@
|
|
|
9
9
|
* renders zero rows while empty and grows to its content while the model
|
|
10
10
|
* has todos.
|
|
11
11
|
* - `activityDoc` — the lastRequest container below the editor
|
|
12
|
-
* (`ui.lastRequest`): the `
|
|
12
|
+
* (`ui.lastRequest`): the ` ● <last request>` line (persisting across
|
|
13
13
|
* agent churn) followed by one compact line PER RUNNING agent. NO box
|
|
14
|
-
* chrome, NO `● Agents` header, NO
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Auto height — it
|
|
14
|
+
* chrome, NO `● Agents` header, NO provider — just
|
|
15
|
+
* `├─ ⠋ <name> · ↻N≤M · 21k/1m · 13.6s · <latest output>`, the `├─ ` /
|
|
16
|
+
* `└─ ` prefix in the same column as the todo rows and the request ` ● `
|
|
17
|
+
* (the last running agent closes the list with `└─ `). Auto height — it
|
|
19
18
|
* collapses to zero rows when both the last-request line is cleared and no
|
|
20
19
|
* agent runs.
|
|
21
20
|
*
|
|
@@ -30,41 +29,64 @@
|
|
|
30
29
|
* `setTheme` recolors in place on a theme hot-switch (no replay buffer — the
|
|
31
30
|
* widget is live state, not transcript history).
|
|
32
31
|
*/
|
|
33
|
-
import { Container } from '@earendil-works/pi-tui';
|
|
32
|
+
import { Container, type Component } from '@earendil-works/pi-tui';
|
|
34
33
|
import type { AgentView } from './dsh-events.ts';
|
|
35
34
|
import { type TuiTheme } from './theme/index.ts';
|
|
36
35
|
/** Refresh interval of the live running-agent lines (spinner + elapsed). */
|
|
37
36
|
export declare const AGENT_TICK_MS = 100;
|
|
38
37
|
/** Braille spinner cycle; the frame index advances once per tick. */
|
|
39
38
|
export declare const AGENT_SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
39
|
+
/**
|
|
40
|
+
* Compact human-readable size: 1_500_000 -> `1.5m`, 20_965 -> `20k`, 999 -> `999`.
|
|
41
|
+
* Millions keep one decimal (a trailing `.0` dropped), thousands floor to `k`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function fmtCompact(n: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* The live Todos panel - a self-drawing table on the panel framework
|
|
46
|
+
* (padCell/columnWidths from panels.ts), rendered at the CURRENT width on
|
|
47
|
+
* every frame. pi-tui's Container calls `render(width)` per frame with no
|
|
48
|
+
* caching, so a terminal resize re-lays the table out (clip + column widths)
|
|
49
|
+
* automatically - no stale baked rows, no word-wrap. Renders zero rows while
|
|
50
|
+
* the list is empty or every todo is completed (clear-when-done).
|
|
51
|
+
*/
|
|
52
|
+
export declare class TodosPanel implements Component {
|
|
53
|
+
private todos;
|
|
54
|
+
private readonly getTheme;
|
|
55
|
+
constructor(getTheme: () => TuiTheme);
|
|
56
|
+
invalidate(): void;
|
|
57
|
+
/** Replace the todos snapshot (`todo/write` events, replay, /new). */
|
|
58
|
+
setTodos(todos: readonly {
|
|
59
|
+
content: string;
|
|
60
|
+
status: string;
|
|
61
|
+
}[]): void;
|
|
62
|
+
render(width: number): string[];
|
|
63
|
+
}
|
|
40
64
|
export declare class LiveWidgets {
|
|
41
65
|
private readonly todosDoc;
|
|
42
66
|
private readonly activityDoc;
|
|
43
67
|
private theme;
|
|
44
68
|
private readonly requestRender;
|
|
45
|
-
/**
|
|
46
|
-
private
|
|
69
|
+
/** The self-drawing Todos table, mounted once in `todosDoc`. */
|
|
70
|
+
private readonly todosPanel;
|
|
47
71
|
/** Latest subagent views from the bridge's onLive fold. */
|
|
48
72
|
private liveAgents;
|
|
49
|
-
/** The Todos panel Text in `todosDoc`, replaced on rebuild. */
|
|
50
|
-
private todosText;
|
|
51
73
|
/** The merged running-agent Text in `activityDoc`, replaced on rebuild. */
|
|
52
74
|
private agentsText;
|
|
53
|
-
/** The `
|
|
75
|
+
/** The ` ● <last request>` Text in `activityDoc` (persists across churn). */
|
|
54
76
|
private requestText;
|
|
55
77
|
/** Plain clipped last-request text backing `requestText` (for setTheme). */
|
|
56
78
|
private requestDisplay;
|
|
57
79
|
/** Spinner frame counter, advanced by tickLive while any agent runs. */
|
|
58
80
|
private spinnerFrame;
|
|
59
81
|
/**
|
|
60
|
-
* @param todosDoc The dock slot ABOVE the chat input
|
|
82
|
+
* @param todosDoc The dock slot ABOVE the chat input - the Todos table
|
|
61
83
|
* panel lives here.
|
|
62
|
-
* @param activityDoc The lastRequest container BELOW the editor
|
|
84
|
+
* @param activityDoc The lastRequest container BELOW the editor - the ` ● `
|
|
63
85
|
* last-request line plus the compact running-agent lines live here.
|
|
64
86
|
*/
|
|
65
87
|
constructor(todosDoc: Container, activityDoc: Container, theme: TuiTheme, requestRender: () => void);
|
|
66
88
|
/**
|
|
67
|
-
* Render the `
|
|
89
|
+
* Render the ` ● <text>` last-request line in `activityDoc`, UNDER which the
|
|
68
90
|
* running-agent lines appear. `undefined` (or blank) removes the line. Same
|
|
69
91
|
* styling as before the merge: `fgMuted` prefix, but the text is clipped to
|
|
70
92
|
* the terminal width (`columns - 5`) so it ALWAYS renders on one row and
|
|
@@ -84,50 +106,38 @@ export declare class LiveWidgets {
|
|
|
84
106
|
* — the activity area then holds its final (empty) state.
|
|
85
107
|
*/
|
|
86
108
|
tickLive(): void;
|
|
87
|
-
/** Recolor the widget (Todos panel,
|
|
88
|
-
* hot-switch (no-op on the same bundle).
|
|
109
|
+
/** Recolor the widget (Todos panel, ● line and agent lines) under a theme
|
|
110
|
+
* hot-switch (no-op on the same bundle). The Todos panel reads the theme
|
|
111
|
+
* through its getter, so the swap needs only a repaint. */
|
|
89
112
|
setTheme(theme: TuiTheme): void;
|
|
90
113
|
/**
|
|
91
114
|
* Drop everything except the last-request echo (`/new`): clears the Todos
|
|
92
|
-
* panel and the running-agent lines, keeps the `
|
|
115
|
+
* panel and the running-agent lines, keeps the ` ● ` line. The bridge also
|
|
93
116
|
* fires `renderAgents([])`.
|
|
94
117
|
*/
|
|
95
118
|
clear(): void;
|
|
96
119
|
/**
|
|
97
|
-
* Rebuild the
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Clear-when-done: the
|
|
103
|
-
* model writes the whole-list snapshot and rarely clears it — an
|
|
104
|
-
* all-completed list is the end-of-work signal); the agent lines hide once
|
|
105
|
-
* no child is running.
|
|
120
|
+
* Rebuild the running-agent surface of `activityDoc`: the ` ● ` line
|
|
121
|
+
* (managed separately, persists) followed by ONE Text holding the compact
|
|
122
|
+
* running-agent lines joined by '\n' (or nothing when no agent runs - the
|
|
123
|
+
* slot collapses to the ● line). The Todos table panel is NOT rebuilt
|
|
124
|
+
* here - it is a self-drawing component that re-renders each frame.
|
|
125
|
+
* Clear-when-done: the agent lines hide once no child is running.
|
|
106
126
|
*/
|
|
107
127
|
private rebuild;
|
|
108
|
-
/** One bordered panel: top border + header row + body rows + bottom border. */
|
|
109
|
-
private boxedPanel;
|
|
110
|
-
/** `● Todos (done/total)`, styled for the panel header row. */
|
|
111
|
-
private todosHeader;
|
|
112
|
-
/**
|
|
113
|
-
* Tree-style todo body lines: `├─`/`└─` connectors with `☐`/`◐`/`☑` status
|
|
114
|
-
* icons. Content is model-controlled: clipped BEFORE styling to the boxed
|
|
115
|
-
* row's inner budget minus the tree chrome (connector 3 cols + icon+space
|
|
116
|
-
* 2 cols) — see clipPanelLine's contract.
|
|
117
|
-
*/
|
|
118
|
-
private todoLines;
|
|
119
128
|
/**
|
|
120
|
-
* One compact running-agent line for the last-request area:
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* the
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
129
|
+
* One compact running-agent line for the last-request area, todo-style:
|
|
130
|
+
* `├─ `/`└─ ` connector (same column as the todo rows and the request
|
|
131
|
+
* ` ● `) + spinner + the agent NAME (`view.label`, matching the boxed
|
|
132
|
+
* panel's main line) + the exact meta that main line showed (`↻retries≤max`,
|
|
133
|
+
* compact `tokens[/contextWindow]`, elapsed) + the child's latest output
|
|
134
|
+
* line when one exists (` · <tail>`, so the user sees it is alive). NO box
|
|
135
|
+
* chrome, NO provider — the name is the prominent element; the last running
|
|
136
|
+
* agent closes the list with `└─ `. The label is the only unbounded field;
|
|
137
|
+
* it is clipped against a budget measured from the terminal width so the
|
|
138
|
+
* whole plain line (prefix included) fits, then the assembled plain line is
|
|
139
|
+
* clipped to the terminal width BEFORE any ANSI is applied (clipToWidth
|
|
140
|
+
* counts SGR fragments as visible columns — style last).
|
|
131
141
|
*/
|
|
132
142
|
private compactAgentLine;
|
|
133
143
|
}
|
package/lib/live-widgets.js
CHANGED
|
@@ -9,13 +9,12 @@
|
|
|
9
9
|
* renders zero rows while empty and grows to its content while the model
|
|
10
10
|
* has todos.
|
|
11
11
|
* - `activityDoc` — the lastRequest container below the editor
|
|
12
|
-
* (`ui.lastRequest`): the `
|
|
12
|
+
* (`ui.lastRequest`): the ` ● <last request>` line (persisting across
|
|
13
13
|
* agent churn) followed by one compact line PER RUNNING agent. NO box
|
|
14
|
-
* chrome, NO `● Agents` header, NO
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Auto height — it
|
|
14
|
+
* chrome, NO `● Agents` header, NO provider — just
|
|
15
|
+
* `├─ ⠋ <name> · ↻N≤M · 21k/1m · 13.6s · <latest output>`, the `├─ ` /
|
|
16
|
+
* `└─ ` prefix in the same column as the todo rows and the request ` ● `
|
|
17
|
+
* (the last running agent closes the list with `└─ `). Auto height — it
|
|
19
18
|
* collapses to zero rows when both the last-request line is cleared and no
|
|
20
19
|
* agent runs.
|
|
21
20
|
*
|
|
@@ -31,13 +30,27 @@
|
|
|
31
30
|
* widget is live state, not transcript history).
|
|
32
31
|
*/
|
|
33
32
|
import { Container, Text } from '@earendil-works/pi-tui';
|
|
34
|
-
import {
|
|
33
|
+
import { panelBoxWidth, clipPanelLine } from "./messages.js";
|
|
34
|
+
import { columnWidths, padCell, TABLE_SEP } from "./panels.js";
|
|
35
35
|
import { ansiFg, RESET } from "./theme/index.js";
|
|
36
36
|
import { clipToWidth, visibleWidth } from "./text.js";
|
|
37
37
|
/** Refresh interval of the live running-agent lines (spinner + elapsed). */
|
|
38
38
|
export const AGENT_TICK_MS = 100;
|
|
39
39
|
/** Braille spinner cycle; the frame index advances once per tick. */
|
|
40
40
|
export const AGENT_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
41
|
+
/**
|
|
42
|
+
* Compact human-readable size: 1_500_000 -> `1.5m`, 20_965 -> `20k`, 999 -> `999`.
|
|
43
|
+
* Millions keep one decimal (a trailing `.0` dropped), thousands floor to `k`.
|
|
44
|
+
*/
|
|
45
|
+
export function fmtCompact(n) {
|
|
46
|
+
if (n >= 1_000_000) {
|
|
47
|
+
const m = n / 1_000_000;
|
|
48
|
+
return `${Number.isInteger(m) ? String(m) : m.toFixed(1).replace(/\.0$/, '')}m`;
|
|
49
|
+
}
|
|
50
|
+
if (n >= 1_000)
|
|
51
|
+
return `${Math.floor(n / 1_000)}k`;
|
|
52
|
+
return String(n);
|
|
53
|
+
}
|
|
41
54
|
/** One bordered row of the Todos panel: `│ ` + inner + ` │` (no trailing RESET). */
|
|
42
55
|
function borderedRow(boxWidth, borderFg, inner) {
|
|
43
56
|
const pad = Math.max(0, boxWidth - 4 - visibleWidth(inner));
|
|
@@ -47,29 +60,104 @@ function borderedRow(boxWidth, borderFg, inner) {
|
|
|
47
60
|
function panelTopBorder(boxWidth, borderFg) {
|
|
48
61
|
return `${borderFg}┌${'─'.repeat(Math.max(0, boxWidth - 2))}┐`;
|
|
49
62
|
}
|
|
63
|
+
/** The todos table columns: right-aligned row number, status icon, flex content. */
|
|
64
|
+
const TODO_COLUMNS = [
|
|
65
|
+
{ key: 'idx', title: '#', width: 3, align: 'right' },
|
|
66
|
+
{ key: 'status', title: '✓', width: 2 },
|
|
67
|
+
{ key: 'content', title: 'Task', flex: true },
|
|
68
|
+
];
|
|
69
|
+
/** Status icon of one todo: pending / in-progress / completed. */
|
|
70
|
+
function todoIcon(status) {
|
|
71
|
+
if (status === 'completed')
|
|
72
|
+
return '☑';
|
|
73
|
+
if (status === 'in_progress')
|
|
74
|
+
return '◐';
|
|
75
|
+
return '☐';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The live Todos panel - a self-drawing table on the panel framework
|
|
79
|
+
* (padCell/columnWidths from panels.ts), rendered at the CURRENT width on
|
|
80
|
+
* every frame. pi-tui's Container calls `render(width)` per frame with no
|
|
81
|
+
* caching, so a terminal resize re-lays the table out (clip + column widths)
|
|
82
|
+
* automatically - no stale baked rows, no word-wrap. Renders zero rows while
|
|
83
|
+
* the list is empty or every todo is completed (clear-when-done).
|
|
84
|
+
*/
|
|
85
|
+
export class TodosPanel {
|
|
86
|
+
todos = [];
|
|
87
|
+
getTheme;
|
|
88
|
+
constructor(getTheme) {
|
|
89
|
+
this.getTheme = getTheme;
|
|
90
|
+
}
|
|
91
|
+
invalidate() { }
|
|
92
|
+
/** Replace the todos snapshot (`todo/write` events, replay, /new). */
|
|
93
|
+
setTodos(todos) {
|
|
94
|
+
this.todos = todos;
|
|
95
|
+
}
|
|
96
|
+
render(width) {
|
|
97
|
+
if (this.todos.length === 0 || this.todos.every(todo => todo.status === 'completed'))
|
|
98
|
+
return [];
|
|
99
|
+
const theme = this.getTheme();
|
|
100
|
+
const p = theme.palette;
|
|
101
|
+
const boxWidth = panelBoxWidth(width);
|
|
102
|
+
const borderFg = ansiFg(p.borderDefault);
|
|
103
|
+
const innerWidth = boxWidth - 4;
|
|
104
|
+
const colWidths = columnWidths(innerWidth, TODO_COLUMNS);
|
|
105
|
+
const subtle = (text) => ansiFg(p.fgSubtle) + text + RESET;
|
|
106
|
+
// Header row of the panel: `● Todos (done/total)` in the accent/subtle pair.
|
|
107
|
+
const done = this.todos.filter(todo => todo.status === 'completed').length;
|
|
108
|
+
const headerInner = ansiFg(p.accent) + '● Todos ' + RESET
|
|
109
|
+
+ subtle(`(${done}/${this.todos.length})`);
|
|
110
|
+
const out = [panelTopBorder(boxWidth, borderFg), borderedRow(boxWidth, borderFg, headerInner)];
|
|
111
|
+
// Table header: every cell padded to its column so the rows align.
|
|
112
|
+
out.push(borderedRow(boxWidth, borderFg, TODO_COLUMNS.map((column, i) => padCell(column.title, colWidths[i], column.align)).join(TABLE_SEP)));
|
|
113
|
+
for (let i = 0; i < this.todos.length; i++) {
|
|
114
|
+
const todo = this.todos[i];
|
|
115
|
+
// Plain (clipped+padded) cells FIRST, ANSI after - the panel-line rule.
|
|
116
|
+
const idxCell = padCell(String(i + 1), colWidths[0], 'right');
|
|
117
|
+
const iconCell = padCell(todoIcon(todo.status), colWidths[1]);
|
|
118
|
+
const contentCell = padClip(todo.content, colWidths[2]);
|
|
119
|
+
// One color per status across the icon and content cells (the icon has
|
|
120
|
+
// its own column here - no baked-in prefix from theme.chat.todo*).
|
|
121
|
+
const color = todo.status === 'completed'
|
|
122
|
+
? p.success
|
|
123
|
+
: todo.status === 'in_progress' ? p.attention : p.fgSubtle;
|
|
124
|
+
const statusStyle = (text) => ansiFg(color) + text + RESET;
|
|
125
|
+
out.push(borderedRow(boxWidth, borderFg, subtle(idxCell) + TABLE_SEP + statusStyle(iconCell) + TABLE_SEP + statusStyle(contentCell)));
|
|
126
|
+
}
|
|
127
|
+
out.push(panelBottomRow(boxWidth, borderFg));
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Bottom border line (`└─…─┘`), `boxWidth` columns wide. */
|
|
132
|
+
function panelBottomRow(boxWidth, borderFg) {
|
|
133
|
+
return `${borderFg}└${'─'.repeat(Math.max(0, boxWidth - 2))}┘`;
|
|
134
|
+
}
|
|
135
|
+
/** Clip one todo's content (a possibly multiline model string) to one row. */
|
|
136
|
+
function padClip(content, width) {
|
|
137
|
+
const flat = content.replace(/\r/g, '').replace(/\n/g, ' ').trim();
|
|
138
|
+
return padCell(flat, width);
|
|
139
|
+
}
|
|
50
140
|
export class LiveWidgets {
|
|
51
141
|
todosDoc;
|
|
52
142
|
activityDoc;
|
|
53
143
|
theme;
|
|
54
144
|
requestRender;
|
|
55
|
-
/**
|
|
56
|
-
|
|
145
|
+
/** The self-drawing Todos table, mounted once in `todosDoc`. */
|
|
146
|
+
todosPanel;
|
|
57
147
|
/** Latest subagent views from the bridge's onLive fold. */
|
|
58
148
|
liveAgents = [];
|
|
59
|
-
/** The Todos panel Text in `todosDoc`, replaced on rebuild. */
|
|
60
|
-
todosText;
|
|
61
149
|
/** The merged running-agent Text in `activityDoc`, replaced on rebuild. */
|
|
62
150
|
agentsText;
|
|
63
|
-
/** The `
|
|
151
|
+
/** The ` ● <last request>` Text in `activityDoc` (persists across churn). */
|
|
64
152
|
requestText;
|
|
65
153
|
/** Plain clipped last-request text backing `requestText` (for setTheme). */
|
|
66
154
|
requestDisplay;
|
|
67
155
|
/** Spinner frame counter, advanced by tickLive while any agent runs. */
|
|
68
156
|
spinnerFrame = 0;
|
|
69
157
|
/**
|
|
70
|
-
* @param todosDoc The dock slot ABOVE the chat input
|
|
158
|
+
* @param todosDoc The dock slot ABOVE the chat input - the Todos table
|
|
71
159
|
* panel lives here.
|
|
72
|
-
* @param activityDoc The lastRequest container BELOW the editor
|
|
160
|
+
* @param activityDoc The lastRequest container BELOW the editor - the ` ● `
|
|
73
161
|
* last-request line plus the compact running-agent lines live here.
|
|
74
162
|
*/
|
|
75
163
|
constructor(todosDoc, activityDoc, theme, requestRender) {
|
|
@@ -77,9 +165,14 @@ export class LiveWidgets {
|
|
|
77
165
|
this.activityDoc = activityDoc;
|
|
78
166
|
this.theme = theme;
|
|
79
167
|
this.requestRender = requestRender;
|
|
168
|
+
// Mounted once: the panel re-renders at the live width every frame and
|
|
169
|
+
// reads the theme through the getter, so theme swaps and terminal
|
|
170
|
+
// resizes need no rebuild wiring here.
|
|
171
|
+
this.todosPanel = new TodosPanel(() => this.theme);
|
|
172
|
+
todosDoc.addChild(this.todosPanel);
|
|
80
173
|
}
|
|
81
174
|
/**
|
|
82
|
-
* Render the `
|
|
175
|
+
* Render the ` ● <text>` last-request line in `activityDoc`, UNDER which the
|
|
83
176
|
* running-agent lines appear. `undefined` (or blank) removes the line. Same
|
|
84
177
|
* styling as before the merge: `fgMuted` prefix, but the text is clipped to
|
|
85
178
|
* the terminal width (`columns - 5`) so it ALWAYS renders on one row and
|
|
@@ -95,14 +188,14 @@ export class LiveWidgets {
|
|
|
95
188
|
}
|
|
96
189
|
return;
|
|
97
190
|
}
|
|
98
|
-
// One-row budget: paddingX=1 on each side (2 cols) + the `
|
|
191
|
+
// One-row budget: paddingX=1 on each side (2 cols) + the ` ● ` prefix (3
|
|
99
192
|
// cols) → the plain `display` must fit `columns - 5`. Non-TTY fallback
|
|
100
193
|
// keeps the old 200-col behavior.
|
|
101
194
|
const budget = Math.max(1, (process.stdout.columns ?? 200) - 5);
|
|
102
195
|
const display = clipToWidth(text, budget);
|
|
103
196
|
if (this.requestText === undefined) {
|
|
104
197
|
this.requestText = new Text('', 1, 0);
|
|
105
|
-
// Keep the
|
|
198
|
+
// Keep the ● line ABOVE the running-agent lines regardless of whether
|
|
106
199
|
// they already render (no insert-at on a plain Container — remove and
|
|
107
200
|
// re-add both in the right order).
|
|
108
201
|
if (this.agentsText !== undefined)
|
|
@@ -112,13 +205,13 @@ export class LiveWidgets {
|
|
|
112
205
|
this.activityDoc.addChild(this.agentsText);
|
|
113
206
|
}
|
|
114
207
|
this.requestDisplay = display;
|
|
115
|
-
this.requestText.setText(ansiFg(this.theme.palette.fgMuted) + `
|
|
208
|
+
this.requestText.setText(ansiFg(this.theme.palette.fgMuted) + ` ● ${display}` + RESET);
|
|
116
209
|
this.requestRender();
|
|
117
210
|
}
|
|
118
211
|
/** Replace the todos snapshot (`todo/write` events and replay). */
|
|
119
212
|
renderTodos(todos) {
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
213
|
+
this.todosPanel.setTodos(todos);
|
|
214
|
+
this.requestRender();
|
|
122
215
|
}
|
|
123
216
|
/** Replace the subagent views (the bridge's onLive fold). */
|
|
124
217
|
renderAgents(agents) {
|
|
@@ -136,32 +229,29 @@ export class LiveWidgets {
|
|
|
136
229
|
this.spinnerFrame += 1;
|
|
137
230
|
this.rebuild();
|
|
138
231
|
}
|
|
139
|
-
/** Recolor the widget (Todos panel,
|
|
140
|
-
* hot-switch (no-op on the same bundle).
|
|
232
|
+
/** Recolor the widget (Todos panel, ● line and agent lines) under a theme
|
|
233
|
+
* hot-switch (no-op on the same bundle). The Todos panel reads the theme
|
|
234
|
+
* through its getter, so the swap needs only a repaint. */
|
|
141
235
|
setTheme(theme) {
|
|
142
236
|
if (theme === this.theme)
|
|
143
237
|
return;
|
|
144
238
|
this.theme = theme;
|
|
145
239
|
this.rebuild();
|
|
146
|
-
// The
|
|
240
|
+
// The request line is not part of rebuild() (it persists independent of the
|
|
147
241
|
// agent lines); recolor it here from the cached plain text.
|
|
148
242
|
if (this.requestDisplay !== undefined && this.requestText !== undefined) {
|
|
149
|
-
this.requestText.setText(ansiFg(this.theme.palette.fgMuted) + `
|
|
243
|
+
this.requestText.setText(ansiFg(this.theme.palette.fgMuted) + ` ● ${this.requestDisplay}` + RESET);
|
|
150
244
|
}
|
|
151
245
|
}
|
|
152
246
|
/**
|
|
153
247
|
* Drop everything except the last-request echo (`/new`): clears the Todos
|
|
154
|
-
* panel and the running-agent lines, keeps the `
|
|
248
|
+
* panel and the running-agent lines, keeps the ` ● ` line. The bridge also
|
|
155
249
|
* fires `renderAgents([])`.
|
|
156
250
|
*/
|
|
157
251
|
clear() {
|
|
158
|
-
this.liveTodos = [];
|
|
159
252
|
this.liveAgents = [];
|
|
160
253
|
this.spinnerFrame = 0;
|
|
161
|
-
|
|
162
|
-
this.todosDoc.removeChild(this.todosText);
|
|
163
|
-
this.todosText = undefined;
|
|
164
|
-
}
|
|
254
|
+
this.todosPanel.setTodos([]);
|
|
165
255
|
if (this.agentsText !== undefined) {
|
|
166
256
|
this.activityDoc.removeChild(this.agentsText);
|
|
167
257
|
this.agentsText = undefined;
|
|
@@ -169,38 +259,24 @@ export class LiveWidgets {
|
|
|
169
259
|
this.requestRender();
|
|
170
260
|
}
|
|
171
261
|
/**
|
|
172
|
-
* Rebuild the
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Clear-when-done: the
|
|
178
|
-
* model writes the whole-list snapshot and rarely clears it — an
|
|
179
|
-
* all-completed list is the end-of-work signal); the agent lines hide once
|
|
180
|
-
* no child is running.
|
|
262
|
+
* Rebuild the running-agent surface of `activityDoc`: the ` ● ` line
|
|
263
|
+
* (managed separately, persists) followed by ONE Text holding the compact
|
|
264
|
+
* running-agent lines joined by '\n' (or nothing when no agent runs - the
|
|
265
|
+
* slot collapses to the ● line). The Todos table panel is NOT rebuilt
|
|
266
|
+
* here - it is a self-drawing component that re-renders each frame.
|
|
267
|
+
* Clear-when-done: the agent lines hide once no child is running.
|
|
181
268
|
*/
|
|
182
269
|
rebuild() {
|
|
183
|
-
if (this.liveTodos.some(todo => todo.status !== 'completed')) {
|
|
184
|
-
const panel = this.boxedPanel(this.todosHeader(), this.todoLines());
|
|
185
|
-
if (this.todosText !== undefined)
|
|
186
|
-
this.todosDoc.removeChild(this.todosText);
|
|
187
|
-
this.todosText = new Text(panel, 1, 0);
|
|
188
|
-
this.todosDoc.addChild(this.todosText);
|
|
189
|
-
}
|
|
190
|
-
else if (this.todosText !== undefined) {
|
|
191
|
-
this.todosDoc.removeChild(this.todosText);
|
|
192
|
-
this.todosText = undefined;
|
|
193
|
-
}
|
|
194
270
|
const running = this.liveAgents.filter(view => view.outcome === undefined);
|
|
195
271
|
if (running.length > 0) {
|
|
196
|
-
const lines = running.map(view => this.compactAgentLine(view));
|
|
272
|
+
const lines = running.map((view, i) => this.compactAgentLine(view, i === running.length - 1));
|
|
197
273
|
const text = lines.join('\n');
|
|
198
274
|
if (this.agentsText !== undefined) {
|
|
199
275
|
this.agentsText.setText(text);
|
|
200
276
|
}
|
|
201
277
|
else {
|
|
202
278
|
this.agentsText = new Text(text, 1, 0);
|
|
203
|
-
// Append under the
|
|
279
|
+
// Append under the ● line when it is present (plain Container appends).
|
|
204
280
|
this.activityDoc.addChild(this.agentsText);
|
|
205
281
|
}
|
|
206
282
|
}
|
|
@@ -210,64 +286,24 @@ export class LiveWidgets {
|
|
|
210
286
|
}
|
|
211
287
|
this.requestRender();
|
|
212
288
|
}
|
|
213
|
-
/** One bordered panel: top border + header row + body rows + bottom border. */
|
|
214
|
-
boxedPanel(headerInner, bodyLines) {
|
|
215
|
-
const boxWidth = panelBoxWidth(process.stdout.columns);
|
|
216
|
-
const borderFg = ansiFg(this.theme.palette.borderDefault);
|
|
217
|
-
const top = panelTopBorder(boxWidth, borderFg);
|
|
218
|
-
const header = borderedRow(boxWidth, borderFg, clipPanelLine(headerInner));
|
|
219
|
-
// 'all' body: every line kept verbatim (each already clipped to one
|
|
220
|
-
// physical row), bottom border appended.
|
|
221
|
-
const body = panelBodyText(bodyLines, boxWidth, borderFg, 'all');
|
|
222
|
-
return `${top}\n${header}\n${body}`;
|
|
223
|
-
}
|
|
224
|
-
/** `● Todos (done/total)`, styled for the panel header row. */
|
|
225
|
-
todosHeader() {
|
|
226
|
-
const done = this.liveTodos.filter(todo => todo.status === 'completed').length;
|
|
227
|
-
return ansiFg(this.theme.palette.accent) + '● Todos ' + RESET
|
|
228
|
-
+ ansiFg(this.theme.palette.fgSubtle) + `(${done}/${this.liveTodos.length})` + RESET;
|
|
229
|
-
}
|
|
230
289
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
290
|
+
* One compact running-agent line for the last-request area, todo-style:
|
|
291
|
+
* `├─ `/`└─ ` connector (same column as the todo rows and the request
|
|
292
|
+
* ` ● `) + spinner + the agent NAME (`view.label`, matching the boxed
|
|
293
|
+
* panel's main line) + the exact meta that main line showed (`↻retries≤max`,
|
|
294
|
+
* compact `tokens[/contextWindow]`, elapsed) + the child's latest output
|
|
295
|
+
* line when one exists (` · <tail>`, so the user sees it is alive). NO box
|
|
296
|
+
* chrome, NO provider — the name is the prominent element; the last running
|
|
297
|
+
* agent closes the list with `└─ `. The label is the only unbounded field;
|
|
298
|
+
* it is clipped against a budget measured from the terminal width so the
|
|
299
|
+
* whole plain line (prefix included) fits, then the assembled plain line is
|
|
300
|
+
* clipped to the terminal width BEFORE any ANSI is applied (clipToWidth
|
|
301
|
+
* counts SGR fragments as visible columns — style last).
|
|
235
302
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
for (let i = 0; i < total; i++) {
|
|
241
|
-
const todo = this.liveTodos[i];
|
|
242
|
-
const content = clipToWidth(todo.content, Math.max(10, innerCap - 5));
|
|
243
|
-
const connector = ansiFg(this.theme.palette.fgSubtle) + (i === total - 1 ? '└─ ' : '├─ ') + RESET;
|
|
244
|
-
const statusStyled = todo.status === 'completed'
|
|
245
|
-
? this.theme.chat.todoDone(content)
|
|
246
|
-
: todo.status === 'in_progress'
|
|
247
|
-
? ansiFg(this.theme.palette.attention) + `◐ ${content}` + RESET
|
|
248
|
-
: this.theme.chat.todoOpen(content);
|
|
249
|
-
lines.push(connector + statusStyled);
|
|
250
|
-
}
|
|
251
|
-
return lines;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* One compact running-agent line for the last-request area: ` ↳ ` prefix
|
|
255
|
-
* (nesting one column right of the ` ↳ ` request text, like a tree level
|
|
256
|
-
* under the last request) + spinner + the agent NAME
|
|
257
|
-
* (`view.label`, matching the boxed panel's main line) + the exact meta that
|
|
258
|
-
* main line showed (`↻retries≤max`, `N token (%)`, elapsed). NO box chrome,
|
|
259
|
-
* NO connector, NO provider — the name is the prominent element. The label
|
|
260
|
-
* is the only unbounded field; it is clipped against a budget measured from
|
|
261
|
-
* the terminal width so the whole plain line (prefix included) fits, then
|
|
262
|
-
* the assembled plain line is clipped to the terminal width BEFORE any ANSI
|
|
263
|
-
* is applied (clipToWidth counts SGR fragments as visible columns — style
|
|
264
|
-
* last). The provider (`view.provider`) is dropped from this line.
|
|
265
|
-
*/
|
|
266
|
-
compactAgentLine(view) {
|
|
267
|
-
// two spaces + ↳ + space: nests one column right of the request line's
|
|
268
|
-
// ` ↳ ` (the request renders as paddingX(1) + ` ↳ `), like a tree level
|
|
269
|
-
// under the last request, so the agent names align under that level.
|
|
270
|
-
const PREFIX = ' ↳ ';
|
|
303
|
+
compactAgentLine(view, isLast) {
|
|
304
|
+
// Todo-style tree connector: `├─ ` for non-final rows, `└─ ` for the last
|
|
305
|
+
// running agent — aligned with the todo rows and the request ` ● `.
|
|
306
|
+
const PREFIX = isLast ? '└─ ' : '├─ ';
|
|
271
307
|
const width = Math.max(1, process.stdout.columns ?? 80);
|
|
272
308
|
const glyph = AGENT_SPINNER_FRAMES[this.spinnerFrame % AGENT_SPINNER_FRAMES.length];
|
|
273
309
|
const metaParts = [];
|
|
@@ -275,20 +311,39 @@ export class LiveWidgets {
|
|
|
275
311
|
metaParts.push(view.maxRetries === undefined ? `↻${view.retries}` : `↻${view.retries}≤${view.maxRetries}`);
|
|
276
312
|
}
|
|
277
313
|
if (view.tokens > 0) {
|
|
278
|
-
|
|
279
|
-
|
|
314
|
+
// Compact tokens[/contextWindow]: `21k/1m` — no percent, no "token".
|
|
315
|
+
const ctx = typeof view.contextWindow === 'number' && view.contextWindow > 0
|
|
316
|
+
? `/${fmtCompact(view.contextWindow)}`
|
|
280
317
|
: '';
|
|
281
|
-
metaParts.push(`${view.tokens}
|
|
318
|
+
metaParts.push(`${fmtCompact(view.tokens)}${ctx}`);
|
|
282
319
|
}
|
|
283
320
|
const elapsed = (Date.now() - view.startedAt) / 1000;
|
|
284
321
|
metaParts.push(`${elapsed.toFixed(1)}s`);
|
|
285
|
-
|
|
286
|
-
//
|
|
322
|
+
let metaPlain = ' · ' + metaParts.join(' · ');
|
|
323
|
+
// Tail: the child's latest visible output line, appended after the elapsed
|
|
324
|
+
// column so the user knows it is alive. Budget ≤30% of the terminal width
|
|
325
|
+
// (clamped 20..60 cols); when the folded line is wider, clip to
|
|
326
|
+
// tailBudget-2 and end with `..` (clipToWidth's own `…` is stripped).
|
|
327
|
+
const lastLine = view.lastLine;
|
|
328
|
+
if (lastLine !== undefined && lastLine !== '') {
|
|
329
|
+
const folded = lastLine.replace(/\s+/g, ' ').trim();
|
|
330
|
+
if (folded !== '') {
|
|
331
|
+
const tailBudget = Math.min(60, Math.max(20, Math.floor(width * 0.3)));
|
|
332
|
+
if (visibleWidth(folded) > tailBudget) {
|
|
333
|
+
const clipped = clipToWidth(folded, tailBudget - 2).replace(/…$/, '');
|
|
334
|
+
metaPlain += ` · ${clipped}..`;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
metaPlain += ` · ${folded}`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Defensive: never render an empty name — fall back to `subagent`.
|
|
287
342
|
const rawLabel = clipPanelLine(view.label, 0).replace(/\r/g, '').trim();
|
|
288
343
|
const namePlain = rawLabel === ''
|
|
289
|
-
?
|
|
344
|
+
? 'subagent'
|
|
290
345
|
: rawLabel;
|
|
291
|
-
// Fixed chrome: prefix (
|
|
346
|
+
// Fixed chrome: prefix (3) + spinner (1) + following space (1) + a safety
|
|
292
347
|
// column. The label takes the remainder of the width budget after meta.
|
|
293
348
|
const fixed = visibleWidth(PREFIX) + 1 + 1 + 1;
|
|
294
349
|
const nameBudget = width - fixed - visibleWidth(metaPlain);
|
|
@@ -297,11 +352,13 @@ export class LiveWidgets {
|
|
|
297
352
|
const plain = PREFIX + glyph + ' ' + nameClipped + metaPlain;
|
|
298
353
|
const clipped = clipToWidth(plain, width);
|
|
299
354
|
// Style segments from the (clipped, guaranteed-identical-when-it-fits)
|
|
300
|
-
// plain pieces: spinner muted, NAME default (prominent),
|
|
355
|
+
// plain pieces: connector subtle, spinner muted, NAME default (prominent),
|
|
356
|
+
// meta (incl. the tail) subtle.
|
|
357
|
+
const connector = ansiFg(this.theme.palette.fgSubtle) + PREFIX + RESET;
|
|
301
358
|
const spinner = ansiFg(this.theme.palette.fgMuted) + glyph + RESET;
|
|
302
359
|
const metaStyled = ansiFg(this.theme.palette.fgSubtle) + metaPlain + RESET;
|
|
303
360
|
const nameStyled = ansiFg(this.theme.palette.fgDefault) + nameClipped + RESET;
|
|
304
|
-
const base =
|
|
361
|
+
const base = connector + spinner + ' ' + nameStyled + metaStyled;
|
|
305
362
|
// If clipping truncated the plain line (pathological meta), re-clip the
|
|
306
363
|
// assembled styled line's visible width too; otherwise base is exact.
|
|
307
364
|
if (visibleWidth(clipped) < visibleWidth(plain)) {
|