@thoth-agents/pi-thoth-theme 0.1.0 → 0.3.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.
Files changed (41) hide show
  1. package/README.md +51 -10
  2. package/dist/index.ts +3967 -0
  3. package/package.json +7 -9
  4. package/src/index.ts +0 -53
  5. package/src/input-box/decorate.ts +0 -304
  6. package/src/input-box/frame.ts +0 -182
  7. package/src/input-box/gradient.ts +0 -92
  8. package/src/input-box/state.ts +0 -90
  9. package/src/render-kit/index.ts +0 -305
  10. package/src/render-kit/working.ts +0 -25
  11. package/src/shared/cache.ts +0 -21
  12. package/src/shared/config.ts +0 -86
  13. package/src/shared/duration.ts +0 -1
  14. package/src/shared/icons.ts +0 -89
  15. package/src/status-line/cost.ts +0 -56
  16. package/src/status-line/index.ts +0 -242
  17. package/src/status-line/layout.ts +0 -272
  18. package/src/status-line/snapshot.ts +0 -11
  19. package/src/status-line/throughput.ts +0 -61
  20. package/src/status-line/tokens.ts +0 -51
  21. package/src/tools/bash.ts +0 -237
  22. package/src/tools/border.ts +0 -21
  23. package/src/tools/box.ts +0 -258
  24. package/src/tools/edit.ts +0 -228
  25. package/src/tools/file-icons.ts +0 -139
  26. package/src/tools/find.ts +0 -227
  27. package/src/tools/frame.ts +0 -166
  28. package/src/tools/generic.ts +0 -195
  29. package/src/tools/grep.ts +0 -371
  30. package/src/tools/index.ts +0 -135
  31. package/src/tools/ls.ts +0 -231
  32. package/src/tools/ownership.ts +0 -70
  33. package/src/tools/powershell.ts +0 -1
  34. package/src/tools/read.ts +0 -194
  35. package/src/tools/ticker.ts +0 -101
  36. package/src/tools/write.ts +0 -188
  37. package/src/welcome/age.ts +0 -11
  38. package/src/welcome/index.ts +0 -132
  39. package/src/welcome/logo.ts +0 -35
  40. package/src/welcome/render.ts +0 -323
  41. package/src/welcome/resources.ts +0 -251
package/src/tools/grep.ts DELETED
@@ -1,371 +0,0 @@
1
- import type {
2
- AgentToolResult,
3
- GrepToolDetails,
4
- Theme,
5
- ToolRenderResultOptions,
6
- } from '@earendil-works/pi-coding-agent';
7
- import { truncateToWidth } from '@earendil-works/pi-tui';
8
- import { toolFooter } from '../render-kit/index.ts';
9
- import type { ThemeConfig } from '../shared/config.ts';
10
- import { icon as semanticIcon, statusIcon } from '../shared/icons.ts';
11
- import { getToolBorderTone } from './border.ts';
12
- import {
13
- createComponent,
14
- escapeControlCharacters,
15
- getRawResultText,
16
- renderBox,
17
- splitResultNotice,
18
- } from './box.ts';
19
- import { getFileIcon, getToolIcon } from './file-icons.ts';
20
- import {
21
- formatDisplayPath,
22
- hasToolResult,
23
- isFramedContext,
24
- renderFrameBottom,
25
- renderFrameRow,
26
- renderFrameTop,
27
- } from './frame.ts';
28
- import { type ElapsedRenderContext, syncElapsedTicker } from './ticker.ts';
29
-
30
- interface GrepArgs {
31
- pattern?: string;
32
- path?: string;
33
- }
34
-
35
- interface GrepContext extends ElapsedRenderContext {
36
- cwd?: string;
37
- lastComponent?: { invalidate?: () => void };
38
- state?: {
39
- hasResult?: boolean;
40
- callComponent?: { invalidate?: () => void };
41
- [key: string]: unknown;
42
- };
43
- toolCallId?: string;
44
- }
45
-
46
- interface GrepMatchItem {
47
- file: string;
48
- line: string;
49
- content: string;
50
- isContext?: boolean;
51
- }
52
-
53
- type GrepParseResult =
54
- | { raw: true; rawLines: string[]; notices: string[] }
55
- | { raw: false; matches: GrepMatchItem[] };
56
-
57
- function getGrepCandidates(
58
- rawLine: string,
59
- isContext: boolean,
60
- ): GrepMatchItem[] {
61
- const candidates: GrepMatchItem[] = [];
62
- // Lookahead includes overlapping delimiters such as :1:2: or -1-2-.
63
- const delimiterPattern = isContext ? /(?=-(\d+)-)/g : /(?=:(\d+):)/g;
64
- for (const delimiter of rawLine.matchAll(delimiterPattern)) {
65
- const file = rawLine.slice(0, delimiter.index);
66
- if (!file) continue;
67
- candidates.push({
68
- file,
69
- line: delimiter[1],
70
- content: rawLine.slice(delimiter.index + delimiter[1].length + 2),
71
- isContext,
72
- });
73
- }
74
- return candidates;
75
- }
76
-
77
- function parseGrepOutput(text: string, details: unknown): GrepParseResult {
78
- const output = splitResultNotice(text, details, 'grep');
79
- const lines =
80
- output.text || output.notices.length > 0 ? output.text.split('\n') : [];
81
- const metadata =
82
- details && typeof details === 'object'
83
- ? (details as GrepToolDetails)
84
- : undefined;
85
- const rawResult: GrepParseResult = {
86
- raw: true,
87
- rawLines: lines,
88
- notices: output.notices,
89
- };
90
- // Limits can remove the match while retaining preceding context. Do not
91
- // interpret any rows from an incomplete SDK result, even if they look unique.
92
- if (
93
- typeof metadata?.matchLimitReached === 'number' ||
94
- metadata?.truncation?.truncated === true ||
95
- metadata?.linesTruncated === true
96
- ) {
97
- return rawResult;
98
- }
99
- const matches: GrepMatchItem[] = [];
100
- for (const rawLine of lines) {
101
- // An orphan context reading is still a possible interpretation: absence of
102
- // a visible match for that filename is not evidence that it is invalid.
103
- const candidates = [
104
- ...getGrepCandidates(rawLine, false),
105
- ...getGrepCandidates(rawLine, true),
106
- ];
107
- if (candidates.length !== 1) return rawResult;
108
- matches.push(candidates[0]);
109
- }
110
-
111
- return { raw: false, matches };
112
- }
113
-
114
- function groupMatches(matches: GrepMatchItem[]): [string, GrepMatchItem[]][] {
115
- const groups: [string, GrepMatchItem[]][] = [];
116
- // Group only contiguous runs. A later occurrence of a file must not move its
117
- // rows ahead of intervening SDK output.
118
- for (const m of matches) {
119
- const previous = groups.at(-1);
120
- if (previous?.[0] === m.file) {
121
- previous[1].push(m);
122
- } else {
123
- groups.push([m.file, [m]]);
124
- }
125
- }
126
- return groups;
127
- }
128
-
129
- export function createCustomGrepTool(cwd: string, config: ThemeConfig) {
130
- return {
131
- renderShell: 'self' as const,
132
- renderCall(rawArgs: unknown, theme: Theme, context: GrepContext) {
133
- syncElapsedTicker(context);
134
- const args = (rawArgs ?? {}) as GrepArgs;
135
- const pattern = escapeControlCharacters(String(args.pattern ?? ''));
136
- const rawPath = args.path ? String(args.path) : '';
137
- const searchPath = rawPath
138
- ? ` in ${escapeControlCharacters(formatDisplayPath(rawPath, context?.cwd ?? cwd))}`
139
- : '';
140
- const icon = getToolIcon('search', config.icons);
141
- const borderTone = getToolBorderTone(context);
142
-
143
- const comp = createComponent((width: number) => {
144
- const safeWidth = Math.max(0, width);
145
- const footer =
146
- context?.executionStarted && context.isPartial
147
- ? toolFooter(theme, { status: 'running', context }, config.icons)
148
- : undefined;
149
- const title = `${theme.fg('accent', icon)} ${theme.bold ? theme.bold(theme.fg('toolTitle', 'Grep')) : theme.fg('toolTitle', 'Grep')} ${theme.fg('syntaxString', `"${pattern}"`)}${theme.fg('dim', searchPath)}`;
150
-
151
- if (!isFramedContext(context)) {
152
- return renderBox(theme, [], safeWidth, {
153
- title,
154
- footer,
155
- isError: borderTone === 'error',
156
- isSuccess: borderTone === 'success',
157
- });
158
- }
159
-
160
- if (hasToolResult(context)) {
161
- return renderFrameTop(theme, title, safeWidth, borderTone);
162
- }
163
-
164
- return [
165
- ...renderFrameTop(theme, title, safeWidth, borderTone),
166
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
167
- ];
168
- });
169
-
170
- if (context?.state) {
171
- context.state.callComponent = comp;
172
- }
173
- return comp;
174
- },
175
-
176
- renderResult(
177
- result: AgentToolResult<unknown>,
178
- options: ToolRenderResultOptions,
179
- theme: Theme,
180
- context: GrepContext,
181
- ) {
182
- if (context?.state) {
183
- context.state.hasResult = true;
184
- if (
185
- context.state.callComponent &&
186
- typeof context.state.callComponent.invalidate === 'function'
187
- ) {
188
- context.state.callComponent.invalidate();
189
- }
190
- }
191
- if (
192
- context?.lastComponent &&
193
- typeof context.lastComponent.invalidate === 'function'
194
- ) {
195
- context.lastComponent.invalidate();
196
- }
197
-
198
- const isErr = Boolean(context?.isError);
199
- const status = options?.isPartial
200
- ? 'running'
201
- : isErr
202
- ? 'failed'
203
- : 'completed';
204
- const footerOptions = {
205
- status,
206
- context: { ...context, isPartial: options?.isPartial },
207
- } as const;
208
- syncElapsedTicker(footerOptions.context);
209
- const borderTone = getToolBorderTone({
210
- isError: isErr,
211
- isPartial: options?.isPartial,
212
- });
213
- const textOutput = getRawResultText(result);
214
-
215
- if (isErr) {
216
- return createComponent((width: number) => {
217
- const safeWidth = Math.max(0, width);
218
- const footer = toolFooter(theme, footerOptions, config.icons);
219
- const errText = `${theme.fg('error', `${statusIcon('warning', config.icons)} `)}${theme.fg('error', escapeControlCharacters(textOutput || 'Grep failed'))}`;
220
- if (!isFramedContext(context)) {
221
- return [
222
- truncateToWidth(errText, safeWidth),
223
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
224
- ];
225
- }
226
- return [
227
- ...renderFrameRow(theme, errText, safeWidth, borderTone),
228
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
229
- ];
230
- });
231
- }
232
-
233
- const output = parseGrepOutput(textOutput, result.details);
234
-
235
- return createComponent((width: number) => {
236
- const safeWidth = Math.max(0, width);
237
- const footer = toolFooter(theme, footerOptions, config.icons);
238
- if (safeWidth === 0) return [];
239
- if (output.raw) {
240
- const { rawLines, notices } = output;
241
- const visibleRawLines = options?.expanded
242
- ? rawLines
243
- : rawLines.slice(0, 8);
244
- const lines = visibleRawLines.map((rawLine) =>
245
- theme.fg('toolOutput', escapeControlCharacters(rawLine)),
246
- );
247
- if (visibleRawLines.length < rawLines.length) {
248
- lines.push(
249
- theme.fg(
250
- 'dim',
251
- `${semanticIcon('ellipsis', config.icons)} ${rawLines.length - visibleRawLines.length} more lines ${semanticIcon('separator', config.icons)} ctrl+o to expand`,
252
- ),
253
- );
254
- }
255
- if (notices.length > 0) lines.push('');
256
- for (const notice of notices) {
257
- lines.push(theme.fg('warning', notice));
258
- }
259
- if (!isFramedContext(context)) {
260
- const rawWithPrefix = lines.map((l) => (l ? ` ${l}` : ' '));
261
- return renderBox(theme, rawWithPrefix, safeWidth, {
262
- footer,
263
- isSuccess: borderTone === 'success',
264
- });
265
- }
266
- return [
267
- ...lines.flatMap((l) =>
268
- renderFrameRow(theme, l ? ` ${l}` : ' ', safeWidth, borderTone),
269
- ),
270
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
271
- ];
272
- }
273
-
274
- const { matches } = output;
275
- const totalItems = matches.length;
276
- if (totalItems === 0) {
277
- if (!isFramedContext(context)) {
278
- return [
279
- truncateToWidth(theme.fg('dim', 'no matches found'), safeWidth),
280
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
281
- ];
282
- }
283
- return [
284
- ...renderFrameRow(
285
- theme,
286
- theme.fg('dim', 'no matches found'),
287
- safeWidth,
288
- borderTone,
289
- ),
290
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
291
- ];
292
- }
293
-
294
- const groupEntries = groupMatches(matches);
295
- const rawLines: string[] = [];
296
-
297
- const maxGroups = options?.expanded ? groupEntries.length : 3;
298
- const visibleGroups = groupEntries.slice(0, maxGroups);
299
- let renderedItemCount = 0;
300
-
301
- for (let gi = 0; gi < visibleGroups.length; gi++) {
302
- const [file, fileMatches] = visibleGroups[gi];
303
- const isLastGroup =
304
- gi === visibleGroups.length - 1 &&
305
- visibleGroups.length === groupEntries.length;
306
- const groupBranch = isLastGroup ? '└─' : '├─';
307
- const fileIcon = getFileIcon(file, config.icons);
308
- const actualMatches = fileMatches.filter((m) => !m.isContext).length;
309
- const matchCountLabel = `(${actualMatches} ${actualMatches === 1 ? 'match' : 'matches'})`;
310
-
311
- const fileHeader = `${theme.fg('dim', groupBranch)} ${theme.fg('accent', fileIcon)} ${theme.fg('toolTitle', escapeControlCharacters(file))} ${theme.fg('dim', matchCountLabel)}`;
312
- rawLines.push(fileHeader);
313
-
314
- const maxMatches = options?.expanded ? fileMatches.length : 2;
315
- const visibleMatches = fileMatches.slice(0, maxMatches);
316
- renderedItemCount += visibleMatches.length;
317
-
318
- const matchBranchPrefix = isLastGroup
319
- ? ' '
320
- : ` ${theme.fg('dim', '│')} `;
321
- for (const m of visibleMatches) {
322
- const separator = m.isContext ? '- ' : ': ';
323
- const lineNum = theme.fg(
324
- 'dim',
325
- `${m.line.padStart(4, ' ')}${separator}`,
326
- );
327
- const content = theme.fg(
328
- m.isContext ? 'dim' : 'toolOutput',
329
- escapeControlCharacters(m.content),
330
- );
331
- rawLines.push(`${matchBranchPrefix}${lineNum}${content}`);
332
- }
333
-
334
- if (
335
- fileMatches.length > visibleMatches.length &&
336
- !options?.expanded
337
- ) {
338
- const moreFileMatches = fileMatches.length - visibleMatches.length;
339
- rawLines.push(
340
- `${matchBranchPrefix}${theme.fg('dim', `${semanticIcon('ellipsis', config.icons)} ${moreFileMatches} more`)}`,
341
- );
342
- }
343
- }
344
-
345
- const remainingItems = totalItems - renderedItemCount;
346
- if (
347
- !options?.expanded &&
348
- (groupEntries.length > maxGroups || remainingItems > 0)
349
- ) {
350
- const hiddenFiles = groupEntries.length - visibleGroups.length;
351
- const summary = `${theme.fg('dim', '└─')} ${theme.fg('dim', `${semanticIcon('ellipsis', config.icons)} ${remainingItems} more matches across ${hiddenFiles} files ${semanticIcon('separator', config.icons)} ctrl+o to expand`)}`;
352
- rawLines.push(summary);
353
- }
354
-
355
- if (!isFramedContext(context)) {
356
- return [
357
- ...rawLines.map((line) => truncateToWidth(` ${line}`, safeWidth)),
358
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
359
- ];
360
- }
361
-
362
- return [
363
- ...rawLines.flatMap((line) =>
364
- renderFrameRow(theme, line, safeWidth, borderTone),
365
- ),
366
- ...renderFrameBottom(theme, footer, safeWidth, borderTone),
367
- ];
368
- });
369
- },
370
- };
371
- }
@@ -1,135 +0,0 @@
1
- import type {
2
- ExtensionAPI,
3
- ToolRendererResolver,
4
- ToolRenderers,
5
- ToolRenderResultOptions,
6
- } from '@earendil-works/pi-coding-agent';
7
- import {
8
- DEFAULT_RESPECT_PACKAGES,
9
- type ThemeConfig,
10
- } from '../shared/config.ts';
11
- import { createCustomBashTool } from './bash.ts';
12
- import { createCustomEditTool } from './edit.ts';
13
- import { createCustomFindTool } from './find.ts';
14
- import { createGenericTool } from './generic.ts';
15
- import { createCustomGrepTool } from './grep.ts';
16
- import { createCustomLsTool } from './ls.ts';
17
- import { createOwnershipResolver } from './ownership.ts';
18
- import { createCustomPowerShellTool } from './powershell.ts';
19
- import { createCustomReadTool } from './read.ts';
20
- import {
21
- activateElapsedTickerOwner,
22
- releaseElapsedTickerOwner,
23
- stopElapsedTickers,
24
- } from './ticker.ts';
25
- import { createCustomWriteTool } from './write.ts';
26
-
27
- /** Resolve themed renderers using this theme instance's live tool ownership. */
28
- export function createToolRendererResolver(
29
- pi: Pick<ExtensionAPI, 'getAllTools'>,
30
- config: ThemeConfig,
31
- cwd = process.cwd(),
32
- ): ToolRendererResolver {
33
- const renderers = new Map<string, ToolRenderers>([
34
- ['read', createCustomReadTool(cwd, config)],
35
- ['bash', createCustomBashTool(cwd, config)],
36
- ['powershell', createCustomPowerShellTool(cwd, config)],
37
- ['ls', createCustomLsTool(cwd, config)],
38
- ['grep', createCustomGrepTool(cwd, config)],
39
- ['find', createCustomFindTool(cwd, config)],
40
- ['edit', createCustomEditTool(cwd, config)],
41
- ['write', createCustomWriteTool(cwd, config)],
42
- ]);
43
-
44
- const genericRenderers = new Map<string, ToolRenderers>();
45
- const isOwnedBaseDir = createOwnershipResolver(
46
- config.tools.respectPackages ?? DEFAULT_RESPECT_PACKAGES,
47
- );
48
-
49
- // Tools absent from the registry cannot be attributed and keep respecting
50
- // their downstream renderers; registered tools are respected only when their
51
- // package matches tools.respectPackages. Read at each resolution so
52
- // replacements by another package are honored.
53
- const canRespectDownstream = (toolName: string): boolean => {
54
- const tool =
55
- typeof pi.getAllTools === 'function'
56
- ? pi.getAllTools().find((t) => t.name === toolName)
57
- : undefined;
58
- if (!tool) return true;
59
- const baseDir = tool.sourceInfo?.baseDir;
60
- return baseDir ? isOwnedBaseDir(baseDir) : false;
61
- };
62
-
63
- return (toolName, next) => {
64
- // Built-ins take precedence over Pi 1.0.1's native callbacks.
65
- const builtIn = renderers.get(toolName);
66
- if (builtIn) return builtIn;
67
-
68
- // Respect configured packages' tools that bring their own renderers.
69
- const downstream = next();
70
- if (
71
- (downstream?.renderCall || downstream?.renderResult) &&
72
- canRespectDownstream(toolName)
73
- ) {
74
- return downstream;
75
- }
76
-
77
- let generic = genericRenderers.get(toolName);
78
- if (!generic) {
79
- generic = createGenericTool(toolName, config);
80
- genericRenderers.set(toolName, generic);
81
- }
82
- return generic;
83
- };
84
- }
85
-
86
- /** Register themed renderers and return an idempotent ticker/subscription disposer. */
87
- export function registerTools(
88
- pi: ExtensionAPI,
89
- config: ThemeConfig,
90
- cwd = process.cwd(),
91
- owner: object = {},
92
- resolveToolRenderers?: ToolRendererResolver,
93
- ): () => void {
94
- if (!config.tools.enabled || typeof pi?.registerToolRenderer !== 'function') {
95
- return () => {};
96
- }
97
-
98
- pi.registerToolRenderer(
99
- resolveToolRenderers ?? createToolRendererResolver(pi, config, cwd),
100
- );
101
-
102
- const unsubs: Array<() => void> = [];
103
- if (typeof pi?.on === 'function') {
104
- unsubs.push(
105
- pi.on('agent_end', (_event, ctx) => {
106
- if (ctx.hasUI) stopElapsedTickers(owner);
107
- }),
108
- );
109
- unsubs.push(
110
- pi.on('session_shutdown', (_event, ctx) => {
111
- if (!ctx.hasUI) return;
112
- stopElapsedTickers(owner);
113
- releaseElapsedTickerOwner(owner);
114
- }),
115
- );
116
- unsubs.push(
117
- pi.on('session_start', (_event, ctx) => {
118
- if (!ctx.hasUI) return;
119
- stopElapsedTickers(owner);
120
- activateElapsedTickerOwner(owner);
121
- }),
122
- );
123
- }
124
-
125
- let disposed = false;
126
- return () => {
127
- if (disposed) return;
128
- disposed = true;
129
- stopElapsedTickers(owner);
130
- releaseElapsedTickerOwner(owner);
131
- for (const unsub of unsubs) unsub();
132
- unsubs.length = 0;
133
- };
134
- }
135
- export type { ToolRenderResultOptions };