@thoth-agents/pi-thoth-theme 0.1.0 → 0.2.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 +11 -5
- package/dist/index.ts +3686 -0
- package/package.json +7 -9
- package/src/index.ts +0 -53
- package/src/input-box/decorate.ts +0 -304
- package/src/input-box/frame.ts +0 -182
- package/src/input-box/gradient.ts +0 -92
- package/src/input-box/state.ts +0 -90
- package/src/render-kit/index.ts +0 -305
- package/src/render-kit/working.ts +0 -25
- package/src/shared/cache.ts +0 -21
- package/src/shared/config.ts +0 -86
- package/src/shared/duration.ts +0 -1
- package/src/shared/icons.ts +0 -89
- package/src/status-line/cost.ts +0 -56
- package/src/status-line/index.ts +0 -242
- package/src/status-line/layout.ts +0 -272
- package/src/status-line/snapshot.ts +0 -11
- package/src/status-line/throughput.ts +0 -61
- package/src/status-line/tokens.ts +0 -51
- package/src/tools/bash.ts +0 -237
- package/src/tools/border.ts +0 -21
- package/src/tools/box.ts +0 -258
- package/src/tools/edit.ts +0 -228
- package/src/tools/file-icons.ts +0 -139
- package/src/tools/find.ts +0 -227
- package/src/tools/frame.ts +0 -166
- package/src/tools/generic.ts +0 -195
- package/src/tools/grep.ts +0 -371
- package/src/tools/index.ts +0 -135
- package/src/tools/ls.ts +0 -231
- package/src/tools/ownership.ts +0 -70
- package/src/tools/powershell.ts +0 -1
- package/src/tools/read.ts +0 -194
- package/src/tools/ticker.ts +0 -101
- package/src/tools/write.ts +0 -188
- package/src/welcome/age.ts +0 -11
- package/src/welcome/index.ts +0 -132
- package/src/welcome/logo.ts +0 -35
- package/src/welcome/render.ts +0 -323
- package/src/welcome/resources.ts +0 -251
package/src/input-box/state.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
-
import type { IconMode } from '../shared/config.ts';
|
|
3
|
-
import { formatDuration } from '../shared/duration.ts';
|
|
4
|
-
import { frames, icon } from '../shared/icons.ts';
|
|
5
|
-
import {
|
|
6
|
-
type BreathingFrame,
|
|
7
|
-
createBreathingFrame,
|
|
8
|
-
renderWorkingIndicator,
|
|
9
|
-
} from './gradient.ts';
|
|
10
|
-
|
|
11
|
-
export function createWorkingState(requestRender: () => void) {
|
|
12
|
-
let startedAt: number | undefined;
|
|
13
|
-
let ticker: ReturnType<typeof setInterval> | undefined;
|
|
14
|
-
let disposed = false;
|
|
15
|
-
|
|
16
|
-
function stop() {
|
|
17
|
-
if (ticker !== undefined) clearInterval(ticker);
|
|
18
|
-
ticker = undefined;
|
|
19
|
-
startedAt = undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
get isWorking() {
|
|
24
|
-
return startedAt !== undefined;
|
|
25
|
-
},
|
|
26
|
-
start() {
|
|
27
|
-
if (disposed) return;
|
|
28
|
-
stop();
|
|
29
|
-
startedAt = Date.now();
|
|
30
|
-
ticker = setInterval(requestRender, 50);
|
|
31
|
-
ticker.unref?.();
|
|
32
|
-
requestRender();
|
|
33
|
-
},
|
|
34
|
-
end() {
|
|
35
|
-
if (disposed) return;
|
|
36
|
-
stop();
|
|
37
|
-
requestRender();
|
|
38
|
-
},
|
|
39
|
-
status(
|
|
40
|
-
indicator: unknown,
|
|
41
|
-
width: number,
|
|
42
|
-
styleMuted: (text: string) => string = (text) => text,
|
|
43
|
-
frame?: BreathingFrame,
|
|
44
|
-
mode: IconMode = 'nerd',
|
|
45
|
-
): string {
|
|
46
|
-
const ready = `${icon('ready', mode)} ready`;
|
|
47
|
-
const separator = icon('separator', mode);
|
|
48
|
-
if (
|
|
49
|
-
!indicator ||
|
|
50
|
-
typeof indicator !== 'object' ||
|
|
51
|
-
!('renderInBorder' in indicator) ||
|
|
52
|
-
typeof indicator.renderInBorder !== 'function'
|
|
53
|
-
) {
|
|
54
|
-
return styleMuted(truncateToWidth(ready, width, ''));
|
|
55
|
-
}
|
|
56
|
-
const working = 'kind' in indicator && indicator.kind === 'working';
|
|
57
|
-
const now = frame?.now ?? Date.now();
|
|
58
|
-
const elapsed =
|
|
59
|
-
working && startedAt !== undefined
|
|
60
|
-
? ` ${separator} ${formatDuration(Math.floor((now - startedAt) / 1000) * 1000)}`
|
|
61
|
-
: '';
|
|
62
|
-
const nativeWidth = Math.max(1, width - visibleWidth(elapsed));
|
|
63
|
-
const native = indicator.renderInBorder(nativeWidth);
|
|
64
|
-
if (typeof native !== 'string' || visibleWidth(native) === 0) {
|
|
65
|
-
return styleMuted(truncateToWidth(ready, width, ''));
|
|
66
|
-
}
|
|
67
|
-
const fitted = truncateToWidth(native, nativeWidth, '');
|
|
68
|
-
const label =
|
|
69
|
-
working && startedAt !== undefined
|
|
70
|
-
? renderWorkingIndicator(
|
|
71
|
-
fitted,
|
|
72
|
-
frame ?? createBreathingFrame(now),
|
|
73
|
-
styleMuted,
|
|
74
|
-
frames('workingFrames', mode),
|
|
75
|
-
)
|
|
76
|
-
: fitted;
|
|
77
|
-
return truncateToWidth(
|
|
78
|
-
`${label}${elapsed ? styleMuted(elapsed) : ''}`,
|
|
79
|
-
width,
|
|
80
|
-
'',
|
|
81
|
-
);
|
|
82
|
-
},
|
|
83
|
-
dispose() {
|
|
84
|
-
disposed = true;
|
|
85
|
-
stop();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export type WorkingState = ReturnType<typeof createWorkingState>;
|
package/src/render-kit/index.ts
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
stripTerminalSequences,
|
|
3
|
-
truncateToWidth,
|
|
4
|
-
wrapTextWithAnsi,
|
|
5
|
-
} from '@earendil-works/pi-tui';
|
|
6
|
-
import type {
|
|
7
|
-
RenderCardOptions,
|
|
8
|
-
RenderIndicatorContext,
|
|
9
|
-
RenderKitTheme,
|
|
10
|
-
RenderRows,
|
|
11
|
-
RenderStatus,
|
|
12
|
-
RenderToolFooterOptions,
|
|
13
|
-
ThothRenderKit,
|
|
14
|
-
} from '@thoth-agents/pi-core';
|
|
15
|
-
import { getToolElapsedMs } from '@thoth-agents/pi-core';
|
|
16
|
-
import { cachedComponent } from '../shared/cache.ts';
|
|
17
|
-
import type { IconMode } from '../shared/config.ts';
|
|
18
|
-
import { formatDuration } from '../shared/duration.ts';
|
|
19
|
-
import { frames, icon, statusIcon } from '../shared/icons.ts';
|
|
20
|
-
import { getBorderTone } from '../tools/border.ts';
|
|
21
|
-
import { renderBox } from '../tools/box.ts';
|
|
22
|
-
import {
|
|
23
|
-
renderFrameBottom,
|
|
24
|
-
renderFrameDivider,
|
|
25
|
-
renderFrameRow,
|
|
26
|
-
renderFrameTop,
|
|
27
|
-
} from '../tools/frame.ts';
|
|
28
|
-
import { syncElapsedTicker } from '../tools/ticker.ts';
|
|
29
|
-
import { runningFooter, workingFrame } from './working.ts';
|
|
30
|
-
|
|
31
|
-
const STATUS_ROLE = {
|
|
32
|
-
pending: 'dim',
|
|
33
|
-
queued: 'dim',
|
|
34
|
-
in_progress: 'accent',
|
|
35
|
-
running: 'accent',
|
|
36
|
-
completed: 'success',
|
|
37
|
-
failed: 'error',
|
|
38
|
-
cancelled: 'muted',
|
|
39
|
-
interrupted: 'warning',
|
|
40
|
-
stopping: 'warning',
|
|
41
|
-
deleted: 'muted',
|
|
42
|
-
blocked: 'warning',
|
|
43
|
-
unknown: 'dim',
|
|
44
|
-
} as const satisfies Record<RenderStatus, string>;
|
|
45
|
-
|
|
46
|
-
/** Terminal statuses whose footer shows the completed or failed icon. */
|
|
47
|
-
const TOOL_FOOTER_STATUS: Partial<
|
|
48
|
-
Record<RenderStatus, 'completed' | 'failed'>
|
|
49
|
-
> = {
|
|
50
|
-
completed: 'completed',
|
|
51
|
-
deleted: 'completed',
|
|
52
|
-
failed: 'failed',
|
|
53
|
-
cancelled: 'failed',
|
|
54
|
-
interrupted: 'failed',
|
|
55
|
-
blocked: 'failed',
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
function startsWithFooterGlyph(footer: string, mode: IconMode): boolean {
|
|
59
|
-
const plain = stripTerminalSequences(footer);
|
|
60
|
-
return (['completed', 'failed'] as const).some((status) => {
|
|
61
|
-
const glyph = statusIcon(status, mode);
|
|
62
|
-
return (
|
|
63
|
-
plain.startsWith(glyph) &&
|
|
64
|
-
(plain.length === glyph.length || /^\s/u.test(plain.slice(glyph.length)))
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isRunning(status: RenderStatus): boolean {
|
|
70
|
-
return status === 'running' || status === 'in_progress';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function statusGlyph(
|
|
74
|
-
theme: RenderKitTheme,
|
|
75
|
-
status: RenderStatus,
|
|
76
|
-
mode: IconMode,
|
|
77
|
-
): string {
|
|
78
|
-
return theme.fg(STATUS_ROLE[status], statusIcon(status, mode));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function indicatorStatus(context: RenderIndicatorContext): RenderStatus {
|
|
82
|
-
if (context.executionStarted && context.isPartial) return 'running';
|
|
83
|
-
if (context.isError) return 'failed';
|
|
84
|
-
return context.executionStarted ? 'completed' : 'pending';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function indicatorLabel(status: RenderStatus): string {
|
|
88
|
-
if (status === 'completed') return 'Done';
|
|
89
|
-
if (status === 'failed') return 'Error';
|
|
90
|
-
return status;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function card(
|
|
94
|
-
theme: RenderKitTheme,
|
|
95
|
-
options: RenderCardOptions,
|
|
96
|
-
width: number,
|
|
97
|
-
toolFooter: NonNullable<ThothRenderKit['toolFooter']>,
|
|
98
|
-
mode: IconMode,
|
|
99
|
-
): string[] {
|
|
100
|
-
const safeWidth = Math.max(0, Math.floor(width));
|
|
101
|
-
if (safeWidth <= 0) return [];
|
|
102
|
-
const bodyWidth = safeWidth <= 4 ? safeWidth : Math.max(1, safeWidth - 4);
|
|
103
|
-
const rows = (source?: RenderRows): string[] => {
|
|
104
|
-
const resolved = typeof source === 'function' ? source(bodyWidth) : source;
|
|
105
|
-
return (resolved ?? []).flatMap((row) =>
|
|
106
|
-
options.wrap ? wrapTextWithAnsi(row, bodyWidth) : [row],
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
let footer = options.footer ?? '';
|
|
110
|
-
if (options.footer === undefined && options.status) {
|
|
111
|
-
footer = options.context
|
|
112
|
-
? toolFooter(theme, {
|
|
113
|
-
status: options.status,
|
|
114
|
-
context: options.context,
|
|
115
|
-
summary: options.summary,
|
|
116
|
-
})
|
|
117
|
-
: statusGlyph(theme, options.status, mode);
|
|
118
|
-
} else if (
|
|
119
|
-
options.status &&
|
|
120
|
-
!isRunning(options.status) &&
|
|
121
|
-
!(TOOL_FOOTER_STATUS[options.status] && startsWithFooterGlyph(footer, mode))
|
|
122
|
-
) {
|
|
123
|
-
footer = [statusGlyph(theme, options.status, mode), footer]
|
|
124
|
-
.filter(Boolean)
|
|
125
|
-
.join(' ');
|
|
126
|
-
}
|
|
127
|
-
const body = rows(options.body);
|
|
128
|
-
const borderState = {
|
|
129
|
-
isError: options.isError,
|
|
130
|
-
isSuccess: options.isSuccess === true,
|
|
131
|
-
};
|
|
132
|
-
if (!options.sections?.length && (!options.part || options.part === 'full')) {
|
|
133
|
-
return renderBox(theme, body, safeWidth, {
|
|
134
|
-
...options,
|
|
135
|
-
...borderState,
|
|
136
|
-
footer,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
const borderTone = getBorderTone(borderState);
|
|
140
|
-
return [
|
|
141
|
-
...(options.part === 'end'
|
|
142
|
-
? []
|
|
143
|
-
: options.title
|
|
144
|
-
? renderFrameTop(theme, options.title, safeWidth, borderTone)
|
|
145
|
-
: renderBox(theme, [], safeWidth, borderState).slice(0, 1)),
|
|
146
|
-
...body.flatMap((row) => renderFrameRow(theme, row, safeWidth, borderTone)),
|
|
147
|
-
...(options.sections ?? []).flatMap((section) => [
|
|
148
|
-
...renderFrameDivider(theme, section.title ?? '', safeWidth, borderTone),
|
|
149
|
-
...rows(section.rows).flatMap((row) =>
|
|
150
|
-
renderFrameRow(theme, row, safeWidth, borderTone),
|
|
151
|
-
),
|
|
152
|
-
]),
|
|
153
|
-
...(options.part === 'start'
|
|
154
|
-
? []
|
|
155
|
-
: renderFrameBottom(theme, footer, safeWidth, borderTone)),
|
|
156
|
-
];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Shared standard footer for kit producers and theme built-ins. */
|
|
160
|
-
export function toolFooter(
|
|
161
|
-
theme: RenderKitTheme,
|
|
162
|
-
options: RenderToolFooterOptions,
|
|
163
|
-
mode: IconMode,
|
|
164
|
-
owner?: object,
|
|
165
|
-
): string {
|
|
166
|
-
syncElapsedTicker(options.context ?? {}, owner, options.status);
|
|
167
|
-
const elapsedMs = getToolElapsedMs(options);
|
|
168
|
-
if (isRunning(options.status)) return runningFooter(theme, elapsedMs, mode);
|
|
169
|
-
const footerStatus = TOOL_FOOTER_STATUS[options.status];
|
|
170
|
-
const glyph = footerStatus ? statusIcon(footerStatus, mode) : undefined;
|
|
171
|
-
const summary =
|
|
172
|
-
typeof options.summary === 'string'
|
|
173
|
-
? [options.summary]
|
|
174
|
-
: (options.summary ?? []);
|
|
175
|
-
const parts = [
|
|
176
|
-
theme.fg(STATUS_ROLE[options.status], glyph ?? options.status),
|
|
177
|
-
];
|
|
178
|
-
if (elapsedMs !== undefined) {
|
|
179
|
-
parts.push(
|
|
180
|
-
theme.fg('dim', formatDuration(Math.floor(elapsedMs / 1000) * 1000)),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (glyph) {
|
|
184
|
-
parts.push(...summary.filter(Boolean).map((part) => theme.fg('dim', part)));
|
|
185
|
-
}
|
|
186
|
-
return parts.join(theme.fg('dim', ` ${icon('separator', mode)} `));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Theme primitives exposed structurally; producers never import this package. */
|
|
190
|
-
export function createRenderKit(
|
|
191
|
-
owner: object,
|
|
192
|
-
resolveToolRenderers?: ThothRenderKit['resolveToolRenderers'],
|
|
193
|
-
mode: IconMode = 'nerd',
|
|
194
|
-
): ThothRenderKit {
|
|
195
|
-
const separator = icon('separator', mode);
|
|
196
|
-
const ellipsis = icon('ellipsis', mode);
|
|
197
|
-
const ownedToolFooter: NonNullable<ThothRenderKit['toolFooter']> = (
|
|
198
|
-
theme,
|
|
199
|
-
options,
|
|
200
|
-
) => toolFooter(theme, options, mode, owner);
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
version: 1,
|
|
204
|
-
...(resolveToolRenderers ? { resolveToolRenderers } : {}),
|
|
205
|
-
toolFooter: ownedToolFooter,
|
|
206
|
-
card(theme, options, width) {
|
|
207
|
-
if (options.footer !== undefined && options.context && options.status) {
|
|
208
|
-
syncElapsedTicker(options.context, owner, options.status);
|
|
209
|
-
}
|
|
210
|
-
return card(theme, options, width, ownedToolFooter, mode);
|
|
211
|
-
},
|
|
212
|
-
collapse(theme, rows, options = {}) {
|
|
213
|
-
const budget = Math.max(0, Math.floor(options.budget ?? 8));
|
|
214
|
-
if (options.expanded || rows.length <= budget) return [...rows];
|
|
215
|
-
return [
|
|
216
|
-
...rows.slice(0, budget),
|
|
217
|
-
theme.fg(
|
|
218
|
-
'dim',
|
|
219
|
-
`${ellipsis} ${rows.length - budget} more lines ${separator} ${options.expandHint ?? 'ctrl+o to expand'}`,
|
|
220
|
-
),
|
|
221
|
-
];
|
|
222
|
-
},
|
|
223
|
-
cachedComponent,
|
|
224
|
-
indicator(theme, context = {}, options = {}) {
|
|
225
|
-
const status = options.status ?? indicatorStatus(context);
|
|
226
|
-
syncElapsedTicker(context, owner, status);
|
|
227
|
-
const running = isRunning(status);
|
|
228
|
-
const elapsedMs = getToolElapsedMs({ ...options, status, context });
|
|
229
|
-
const elapsed =
|
|
230
|
-
elapsedMs === undefined
|
|
231
|
-
? ''
|
|
232
|
-
: formatDuration(
|
|
233
|
-
running ? Math.floor(elapsedMs / 1000) * 1000 : elapsedMs,
|
|
234
|
-
);
|
|
235
|
-
const frame = options.frame;
|
|
236
|
-
const spinner = frames('spinnerFrames', mode);
|
|
237
|
-
const workingGlyph =
|
|
238
|
-
frame !== undefined && Number.isFinite(frame)
|
|
239
|
-
? spinner[
|
|
240
|
-
((Math.floor(frame) % spinner.length) + spinner.length) %
|
|
241
|
-
spinner.length
|
|
242
|
-
]
|
|
243
|
-
: workingFrame(elapsedMs, mode);
|
|
244
|
-
const glyph = running
|
|
245
|
-
? theme.fg('accent', workingGlyph)
|
|
246
|
-
: statusGlyph(theme, status, mode);
|
|
247
|
-
const label = options.label ?? indicatorLabel(status);
|
|
248
|
-
const role = STATUS_ROLE[status];
|
|
249
|
-
return {
|
|
250
|
-
glyph,
|
|
251
|
-
elapsedMs,
|
|
252
|
-
elapsed,
|
|
253
|
-
text: running
|
|
254
|
-
? ownedToolFooter(theme, { status, elapsedMs })
|
|
255
|
-
: [theme.fg(role, label), elapsed ? theme.fg('dim', elapsed) : '']
|
|
256
|
-
.filter(Boolean)
|
|
257
|
-
.join(theme.fg('dim', ` ${separator} `)),
|
|
258
|
-
};
|
|
259
|
-
},
|
|
260
|
-
icon(name) {
|
|
261
|
-
return name === 'spinnerFrames' || name === 'workingFrames'
|
|
262
|
-
? frames(name, mode)
|
|
263
|
-
: icon(name, mode);
|
|
264
|
-
},
|
|
265
|
-
statusGlyph: (theme, status) => statusGlyph(theme, status, mode),
|
|
266
|
-
widgetHeading(theme, options, width) {
|
|
267
|
-
const title = theme.fg('toolTitle', options.title);
|
|
268
|
-
return truncateToWidth(
|
|
269
|
-
[
|
|
270
|
-
options.status
|
|
271
|
-
? statusGlyph(theme, options.status, mode)
|
|
272
|
-
: theme.fg('accent', icon('ready', mode)),
|
|
273
|
-
theme.bold ? theme.bold(title) : title,
|
|
274
|
-
options.counts
|
|
275
|
-
? theme.fg(
|
|
276
|
-
'dim',
|
|
277
|
-
`${options.counts.completed}/${options.counts.total}`,
|
|
278
|
-
)
|
|
279
|
-
: '',
|
|
280
|
-
options.suffix ? theme.fg('dim', options.suffix) : '',
|
|
281
|
-
]
|
|
282
|
-
.filter(Boolean)
|
|
283
|
-
.join(' '),
|
|
284
|
-
Math.max(0, Math.floor(width)),
|
|
285
|
-
ellipsis,
|
|
286
|
-
);
|
|
287
|
-
},
|
|
288
|
-
treeRow(theme, options, width) {
|
|
289
|
-
const rail = `${' '.repeat(Math.max(0, Math.floor(options.depth ?? 0)))}${options.last ? '└─' : '├─'} `;
|
|
290
|
-
return truncateToWidth(
|
|
291
|
-
[
|
|
292
|
-
theme.fg(
|
|
293
|
-
options.selected ? 'accent' : 'dim',
|
|
294
|
-
`${options.selected ? `${icon('selection', mode)} ` : ' '}${rail}`,
|
|
295
|
-
),
|
|
296
|
-
options.status ? `${statusGlyph(theme, options.status, mode)} ` : '',
|
|
297
|
-
options.text,
|
|
298
|
-
].join(''),
|
|
299
|
-
Math.max(0, Math.floor(width)),
|
|
300
|
-
ellipsis,
|
|
301
|
-
);
|
|
302
|
-
},
|
|
303
|
-
fg: (theme, role, text) => theme.fg(role, text),
|
|
304
|
-
};
|
|
305
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { RenderKitTheme } from '@thoth-agents/pi-core';
|
|
2
|
-
import type { IconMode } from '../shared/config.ts';
|
|
3
|
-
import { formatDuration } from '../shared/duration.ts';
|
|
4
|
-
import { frames, icon } from '../shared/icons.ts';
|
|
5
|
-
|
|
6
|
-
export function workingFrame(elapsedMs = 0, mode: IconMode = 'nerd'): string {
|
|
7
|
-
const elapsed = Number.isFinite(elapsedMs) ? Math.max(0, elapsedMs) : 0;
|
|
8
|
-
const working = frames('workingFrames', mode);
|
|
9
|
-
return working[Math.floor(elapsed / 1000) % working.length];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function runningFooter(
|
|
13
|
-
theme: RenderKitTheme,
|
|
14
|
-
elapsedMs: number | undefined,
|
|
15
|
-
mode: IconMode,
|
|
16
|
-
): string {
|
|
17
|
-
return [
|
|
18
|
-
theme.fg('accent', workingFrame(elapsedMs, mode)),
|
|
19
|
-
elapsedMs === undefined
|
|
20
|
-
? ''
|
|
21
|
-
: theme.fg('dim', formatDuration(Math.floor(elapsedMs / 1000) * 1000)),
|
|
22
|
-
]
|
|
23
|
-
.filter(Boolean)
|
|
24
|
-
.join(theme.fg('dim', ` ${icon('separator', mode)} `));
|
|
25
|
-
}
|
package/src/shared/cache.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { Component } from '@earendil-works/pi-tui';
|
|
2
|
-
|
|
3
|
-
/** Cache lines per instance and width; invalidate when render inputs change. */
|
|
4
|
-
export function cachedComponent(
|
|
5
|
-
renderFn: (width: number) => string[],
|
|
6
|
-
): Component {
|
|
7
|
-
const cache = new Map<number, string[]>();
|
|
8
|
-
return {
|
|
9
|
-
render(width: number): string[] {
|
|
10
|
-
let lines = cache.get(width);
|
|
11
|
-
if (lines === undefined) {
|
|
12
|
-
lines = renderFn(width);
|
|
13
|
-
cache.set(width, lines);
|
|
14
|
-
}
|
|
15
|
-
return lines;
|
|
16
|
-
},
|
|
17
|
-
invalidate(): void {
|
|
18
|
-
cache.clear();
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
}
|
package/src/shared/config.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
export type IconMode = 'nerd' | 'ascii';
|
|
6
|
-
|
|
7
|
-
export const DEFAULT_RESPECT_PACKAGES: readonly string[] = [
|
|
8
|
-
'thoth-agents',
|
|
9
|
-
'@thoth-agents/*',
|
|
10
|
-
'thoth-mem',
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
export interface ThemeConfig {
|
|
14
|
-
icons: IconMode;
|
|
15
|
-
statusLine: {
|
|
16
|
-
enabled: boolean;
|
|
17
|
-
subscriptionProviders?: string[];
|
|
18
|
-
};
|
|
19
|
-
/** Omission enables the input box, preserving the legacy config shape. */
|
|
20
|
-
inputBox?: { enabled: boolean };
|
|
21
|
-
tools: { enabled: boolean; respectPackages?: string[] };
|
|
22
|
-
welcome: { enabled: boolean };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function asObject(value: unknown): Record<string, unknown> {
|
|
26
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
27
|
-
? (value as Record<string, unknown>)
|
|
28
|
-
: {};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function moduleEnabled(value: unknown): boolean {
|
|
32
|
-
const enabled = asObject(value).enabled;
|
|
33
|
-
return typeof enabled === 'boolean' ? enabled : true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseSubscriptionProviders(value: unknown): string[] {
|
|
37
|
-
const obj = asObject(value);
|
|
38
|
-
const providers = obj.subscriptionProviders;
|
|
39
|
-
if (Array.isArray(providers)) {
|
|
40
|
-
return providers.filter(
|
|
41
|
-
(item): item is string =>
|
|
42
|
-
typeof item === 'string' && item.trim().length > 0,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return ['claude-bridge'];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parseRespectPackages(value: unknown): string[] {
|
|
49
|
-
const packages = asObject(value).respectPackages;
|
|
50
|
-
return Array.isArray(packages) &&
|
|
51
|
-
packages.every(
|
|
52
|
-
(item): item is string =>
|
|
53
|
-
typeof item === 'string' && item.trim().length > 0,
|
|
54
|
-
)
|
|
55
|
-
? packages
|
|
56
|
-
: [...DEFAULT_RESPECT_PACKAGES];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function loadConfig(
|
|
60
|
-
configPath = join(
|
|
61
|
-
process.env.PI_CODING_AGENT_DIR || join(homedir(), '.pi', 'agent'),
|
|
62
|
-
'pi-thoth-theme.json',
|
|
63
|
-
),
|
|
64
|
-
): ThemeConfig {
|
|
65
|
-
let config: Record<string, unknown> = {};
|
|
66
|
-
try {
|
|
67
|
-
config = asObject(JSON.parse(readFileSync(configPath, 'utf8')));
|
|
68
|
-
} catch {
|
|
69
|
-
// Missing or malformed package configuration uses package defaults.
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
icons: config.icons === 'ascii' ? 'ascii' : 'nerd',
|
|
73
|
-
...(config.inputBox === undefined
|
|
74
|
-
? {}
|
|
75
|
-
: { inputBox: { enabled: moduleEnabled(config.inputBox) } }),
|
|
76
|
-
statusLine: {
|
|
77
|
-
enabled: moduleEnabled(config.statusLine),
|
|
78
|
-
subscriptionProviders: parseSubscriptionProviders(config.statusLine),
|
|
79
|
-
},
|
|
80
|
-
tools: {
|
|
81
|
-
enabled: moduleEnabled(config.tools),
|
|
82
|
-
respectPackages: parseRespectPackages(config.tools),
|
|
83
|
-
},
|
|
84
|
-
welcome: { enabled: moduleEnabled(config.welcome) },
|
|
85
|
-
};
|
|
86
|
-
}
|
package/src/shared/duration.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { formatDuration } from '@thoth-agents/pi-core';
|
package/src/shared/icons.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
RenderStatus,
|
|
3
|
-
SemanticFrameName,
|
|
4
|
-
SemanticGlyphName,
|
|
5
|
-
} from '@thoth-agents/pi-core';
|
|
6
|
-
import type { IconMode } from './config.ts';
|
|
7
|
-
|
|
8
|
-
type Variants = Record<IconMode, string>;
|
|
9
|
-
|
|
10
|
-
/** Theme-local tool icon that is not a pi-core semantic name. */
|
|
11
|
-
export type ThemeIconName = SemanticGlyphName | 'powershell';
|
|
12
|
-
export type StatusIconName = RenderStatus | 'warning';
|
|
13
|
-
|
|
14
|
-
/** The single source of rendered semantic glyphs for the theme. */
|
|
15
|
-
const ICONS: Record<ThemeIconName, Variants> = {
|
|
16
|
-
branch: { nerd: '\u{e0a0}', ascii: 'git' },
|
|
17
|
-
folder: { nerd: '\u{f07c}', ascii: 'dir' },
|
|
18
|
-
model: { nerd: '\u{f06a9}', ascii: '*' },
|
|
19
|
-
effort: { nerd: '\u{f09d1}', ascii: 'o' },
|
|
20
|
-
context: { nerd: '\u{f2db}', ascii: 'ctx' },
|
|
21
|
-
cost: { nerd: '\u{f155}', ascii: '$' },
|
|
22
|
-
tokensIn: { nerd: '\u{f062}', ascii: '^' },
|
|
23
|
-
tokensOut: { nerd: '\u{f063}', ascii: 'v' },
|
|
24
|
-
cache: { nerd: '\u{f01bc}', ascii: 'cache' },
|
|
25
|
-
throughput: { nerd: '\u{f04c5}', ascii: 'tok/s' },
|
|
26
|
-
agent: { nerd: '\u{f08c7}', ascii: '@' },
|
|
27
|
-
file: { nerd: '\u{f0214}', ascii: '[file]' },
|
|
28
|
-
tool: { nerd: '\u{f0ad}', ascii: '*' },
|
|
29
|
-
bash: { nerd: '\u{f489}', ascii: '$' },
|
|
30
|
-
powershell: { nerd: '\u{e70f}', ascii: 'PS' },
|
|
31
|
-
read: { nerd: '\u{f06e}', ascii: '[read]' },
|
|
32
|
-
write: { nerd: '\u{f0c7}', ascii: '[write]' },
|
|
33
|
-
edit: { nerd: '\u{f044}', ascii: '[edit]' },
|
|
34
|
-
search: { nerd: '\u{f002}', ascii: '?' },
|
|
35
|
-
separator: { nerd: '·', ascii: '|' },
|
|
36
|
-
ellipsis: { nerd: '…', ascii: '...' },
|
|
37
|
-
arrowUp: { nerd: '↑', ascii: '^' },
|
|
38
|
-
arrowDown: { nerd: '↓', ascii: 'v' },
|
|
39
|
-
arrowLeft: { nerd: '←', ascii: '<' },
|
|
40
|
-
arrowRight: { nerd: '→', ascii: '>' },
|
|
41
|
-
selection: { nerd: '›', ascii: '>' },
|
|
42
|
-
scrollUp: { nerd: '↑', ascii: '^' },
|
|
43
|
-
scrollDown: { nerd: '↓', ascii: 'v' },
|
|
44
|
-
ready: { nerd: '▲', ascii: '^' },
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const FRAMES: Record<SemanticFrameName, Record<IconMode, readonly string[]>> = {
|
|
48
|
-
spinnerFrames: {
|
|
49
|
-
nerd: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
50
|
-
ascii: ['|', '/', '-', '\\'],
|
|
51
|
-
},
|
|
52
|
-
workingFrames: {
|
|
53
|
-
nerd: ['△', '◭', '▲', '◮'],
|
|
54
|
-
ascii: ['.', 'o', 'O', '0'],
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const BAN: Variants = { nerd: '\uf05e', ascii: '/' };
|
|
59
|
-
|
|
60
|
-
const STATUS_ICONS: Record<StatusIconName, Variants> = {
|
|
61
|
-
pending: { nerd: '\u{f10c}', ascii: '-' },
|
|
62
|
-
queued: { nerd: '\u{f051f}', ascii: '~' },
|
|
63
|
-
in_progress: { nerd: '◐', ascii: '*' },
|
|
64
|
-
running: { nerd: '◐', ascii: '*' },
|
|
65
|
-
completed: { nerd: '\u{f00c}', ascii: '+' },
|
|
66
|
-
failed: { nerd: '\u{f00d}', ascii: 'x' },
|
|
67
|
-
cancelled: BAN,
|
|
68
|
-
interrupted: BAN,
|
|
69
|
-
stopping: BAN,
|
|
70
|
-
deleted: BAN,
|
|
71
|
-
blocked: BAN,
|
|
72
|
-
warning: { nerd: '\u{f071}', ascii: '!' },
|
|
73
|
-
unknown: { nerd: '\u{f128}', ascii: '?' },
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export function icon(name: ThemeIconName, mode: IconMode): string {
|
|
77
|
-
return ICONS[name][mode];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function frames(
|
|
81
|
-
name: SemanticFrameName,
|
|
82
|
-
mode: IconMode,
|
|
83
|
-
): readonly string[] {
|
|
84
|
-
return FRAMES[name][mode];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function statusIcon(status: StatusIconName, mode: IconMode): string {
|
|
88
|
-
return STATUS_ICONS[status][mode];
|
|
89
|
-
}
|
package/src/status-line/cost.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import type { IconMode } from '../shared/config.ts';
|
|
3
|
-
import { icon } from '../shared/icons.ts';
|
|
4
|
-
|
|
5
|
-
export interface SessionEntriesSource {
|
|
6
|
-
getEntries(): readonly SessionEntry[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Aggregates cumulative cost across all session entries.
|
|
11
|
-
* Evaluates assistant messages, tool results, compactions, branch summaries,
|
|
12
|
-
* and usage entries.
|
|
13
|
-
*/
|
|
14
|
-
export function calculateSessionCost(
|
|
15
|
-
source: readonly SessionEntry[] | SessionEntriesSource | undefined | null,
|
|
16
|
-
): number {
|
|
17
|
-
if (!source) return 0;
|
|
18
|
-
const entries: readonly SessionEntry[] = Array.isArray(source)
|
|
19
|
-
? source
|
|
20
|
-
: typeof (source as SessionEntriesSource).getEntries === 'function'
|
|
21
|
-
? (source as SessionEntriesSource).getEntries()
|
|
22
|
-
: [];
|
|
23
|
-
|
|
24
|
-
let totalCost = 0;
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
if (!entry) continue;
|
|
27
|
-
if (entry.type === 'message') {
|
|
28
|
-
const msg = entry.message;
|
|
29
|
-
if (
|
|
30
|
-
(msg?.role === 'assistant' || msg?.role === 'toolResult') &&
|
|
31
|
-
'usage' in msg &&
|
|
32
|
-
msg.usage?.cost?.total
|
|
33
|
-
) {
|
|
34
|
-
totalCost += msg.usage.cost.total;
|
|
35
|
-
}
|
|
36
|
-
} else if (
|
|
37
|
-
(entry.type === 'branch_summary' ||
|
|
38
|
-
entry.type === 'compaction' ||
|
|
39
|
-
entry.type === 'usage') &&
|
|
40
|
-
'usage' in entry &&
|
|
41
|
-
entry.usage?.cost?.total
|
|
42
|
-
) {
|
|
43
|
-
totalCost += entry.usage.cost.total;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return totalCost;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Formats a cost number (e.g. 1.234) with 3 decimal places and icon prefix
|
|
52
|
-
* ('$' in ascii mode, nerd dollar icon in nerd mode).
|
|
53
|
-
*/
|
|
54
|
-
export function formatCost(cost: number, mode: IconMode): string {
|
|
55
|
-
return `${icon('cost', mode)}${cost.toFixed(3)}`;
|
|
56
|
-
}
|