@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.
Files changed (41) hide show
  1. package/README.md +11 -5
  2. package/dist/index.ts +3686 -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
@@ -1,166 +0,0 @@
1
- import { relative, resolve } from 'node:path';
2
- import type { Theme } from '@earendil-works/pi-coding-agent';
3
- import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
4
- import type { BorderTone } from './border.ts';
5
-
6
- export const FRAME_HORIZONTAL = '─';
7
- export const FRAME_VERTICAL = '│';
8
- export const FRAME_ROUND_TOP_LEFT = '╭';
9
- export const FRAME_ROUND_TOP_RIGHT = '╮';
10
- export const FRAME_ROUND_BOTTOM_LEFT = '╰';
11
- export const FRAME_ROUND_BOTTOM_RIGHT = '╯';
12
- export const FRAME_DIVIDER_LEFT = '├';
13
- export const FRAME_DIVIDER_RIGHT = '┤';
14
-
15
- export function formatDisplayPath(targetPath: string, cwd?: string): string {
16
- if (!targetPath) return '';
17
- if (!cwd) return targetPath;
18
- try {
19
- const absTarget = resolve(cwd, targetPath);
20
- const absCwd = resolve(cwd);
21
- const rel = relative(absCwd, absTarget);
22
- if (
23
- !rel.startsWith('..') &&
24
- !rel.startsWith('/') &&
25
- !/^[A-Za-z]:/.test(rel)
26
- ) {
27
- // Inside cwd. Normalize to forward slashes for clean unified display.
28
- return rel.replace(/\\/g, '/') || '.';
29
- }
30
- } catch {
31
- // ignore resolution error
32
- }
33
- return targetPath;
34
- }
35
-
36
- export function hasToolResult(context?: {
37
- isError?: boolean;
38
- state?: { hasResult?: boolean; [key: string]: unknown };
39
- }): boolean {
40
- if (!context) return false;
41
- return Boolean(context.state?.hasResult);
42
- }
43
-
44
- export function isFramedContext(context?: {
45
- toolCallId?: string;
46
- state?: { hasResult?: boolean; [key: string]: unknown };
47
- }): boolean {
48
- if (!context) return false;
49
- return Boolean(context.toolCallId || context.state);
50
- }
51
-
52
- export function renderFrameTop(
53
- theme: Pick<Theme, 'fg'>,
54
- title: string,
55
- width: number,
56
- borderColor: BorderTone = 'accent',
57
- ): string[] {
58
- const safeWidth = Math.max(0, Math.floor(width));
59
-
60
- if (safeWidth <= 0) return [];
61
- if (safeWidth <= 4) {
62
- return [truncateToWidth(title, safeWidth)];
63
- }
64
-
65
- const innerWidth = Math.max(1, safeWidth - 2);
66
- const rawTitle = ` ${title} `;
67
- const maxTitleWidth = Math.max(1, innerWidth - 4);
68
- const fitTitle =
69
- visibleWidth(rawTitle) > maxTitleWidth
70
- ? ` ${truncateToWidth(title, Math.max(1, maxTitleWidth - 2))} `
71
- : rawTitle;
72
- const titleWidth = visibleWidth(fitTitle);
73
- const remainingDashes = Math.max(1, innerWidth - 2 - titleWidth);
74
- const top = `${theme.fg(borderColor, `${FRAME_ROUND_TOP_LEFT}${FRAME_HORIZONTAL}${FRAME_HORIZONTAL}`)}${fitTitle}${theme.fg(
75
- borderColor,
76
- `${FRAME_HORIZONTAL.repeat(remainingDashes)}${FRAME_ROUND_TOP_RIGHT}`,
77
- )}`;
78
- return [truncateToWidth(top, safeWidth)];
79
- }
80
-
81
- export function renderFrameRow(
82
- theme: Pick<Theme, 'fg'>,
83
- line: string,
84
- width: number,
85
- borderColor: BorderTone = 'accent',
86
- ): string[] {
87
- const safeWidth = Math.max(0, Math.floor(width));
88
-
89
- if (safeWidth <= 0) return [];
90
- if (safeWidth <= 4) {
91
- return [truncateToWidth(line, safeWidth)];
92
- }
93
-
94
- const innerWidth = Math.max(1, safeWidth - 2);
95
- const contentWidth = Math.max(1, innerWidth - 2);
96
- const truncated = truncateToWidth(line, contentWidth);
97
- const pad = Math.max(0, contentWidth - visibleWidth(truncated));
98
- const framed = `${theme.fg(borderColor, `${FRAME_VERTICAL} `)}${truncated}${' '.repeat(pad)}${theme.fg(borderColor, ` ${FRAME_VERTICAL}`)}`;
99
- return [truncateToWidth(framed, safeWidth)];
100
- }
101
-
102
- export function renderFrameDivider(
103
- theme: Pick<Theme, 'fg'>,
104
- title: string,
105
- width: number,
106
- borderColor: BorderTone = 'accent',
107
- ): string[] {
108
- const safeWidth = Math.max(0, Math.floor(width));
109
-
110
- if (safeWidth <= 0) return [];
111
- if (safeWidth <= 4) {
112
- return [truncateToWidth(title, safeWidth)];
113
- }
114
-
115
- const innerWidth = Math.max(1, safeWidth - 2);
116
- const rawTitle = ` ${title} `;
117
- const maxTitleWidth = Math.max(1, innerWidth - 4);
118
- const fitTitle =
119
- visibleWidth(rawTitle) > maxTitleWidth
120
- ? ` ${truncateToWidth(title, Math.max(1, maxTitleWidth - 2))} `
121
- : rawTitle;
122
- const titleWidth = visibleWidth(fitTitle);
123
- const remainingDashes = Math.max(1, innerWidth - 2 - titleWidth);
124
- const divider = `${theme.fg(borderColor, `${FRAME_DIVIDER_LEFT}${FRAME_HORIZONTAL}${FRAME_HORIZONTAL}`)}${theme.fg('dim', fitTitle)}${theme.fg(
125
- borderColor,
126
- `${FRAME_HORIZONTAL.repeat(remainingDashes)}${FRAME_DIVIDER_RIGHT}`,
127
- )}`;
128
- return [truncateToWidth(divider, safeWidth)];
129
- }
130
-
131
- export function renderFrameBottom(
132
- theme: Pick<Theme, 'fg'>,
133
- footer: string | undefined,
134
- width: number,
135
- borderColor: BorderTone = 'accent',
136
- ): string[] {
137
- const safeWidth = Math.max(0, Math.floor(width));
138
-
139
- if (safeWidth <= 0) return [];
140
- if (safeWidth <= 4) {
141
- return footer ? [truncateToWidth(footer, safeWidth)] : [];
142
- }
143
-
144
- const innerWidth = Math.max(1, safeWidth - 2);
145
- if (footer) {
146
- const rawFooter = ` ${footer} `;
147
- const maxFooterWidth = Math.max(1, innerWidth - 4);
148
- const fitFooter =
149
- visibleWidth(rawFooter) > maxFooterWidth
150
- ? ` ${truncateToWidth(footer, Math.max(1, maxFooterWidth - 2))} `
151
- : rawFooter;
152
- const footerWidth = visibleWidth(fitFooter);
153
- const remainingDashes = Math.max(1, innerWidth - 2 - footerWidth);
154
- const bottom = `${theme.fg(borderColor, `${FRAME_ROUND_BOTTOM_LEFT}${FRAME_HORIZONTAL}${FRAME_HORIZONTAL}`)}${fitFooter}${theme.fg(
155
- borderColor,
156
- `${FRAME_HORIZONTAL.repeat(remainingDashes)}${FRAME_ROUND_BOTTOM_RIGHT}`,
157
- )}`;
158
- return [truncateToWidth(bottom, safeWidth)];
159
- }
160
-
161
- const bottom = `${theme.fg(
162
- borderColor,
163
- `${FRAME_ROUND_BOTTOM_LEFT}${FRAME_HORIZONTAL.repeat(innerWidth)}${FRAME_ROUND_BOTTOM_RIGHT}`,
164
- )}`;
165
- return [truncateToWidth(bottom, safeWidth)];
166
- }
@@ -1,195 +0,0 @@
1
- import type {
2
- AgentToolResult,
3
- Theme,
4
- ToolRenderers,
5
- ToolRenderResultOptions,
6
- } from '@earendil-works/pi-coding-agent';
7
- import { toolFooter } from '../render-kit/index.ts';
8
- import type { IconMode, ThemeConfig } from '../shared/config.ts';
9
- import { icon as semanticIcon } from '../shared/icons.ts';
10
- import { getToolBorderTone } from './border.ts';
11
- import {
12
- createComponent,
13
- escapeControlCharacters,
14
- getResultText,
15
- hasImageContent,
16
- } from './box.ts';
17
- import {
18
- hasToolResult,
19
- renderFrameBottom,
20
- renderFrameRow,
21
- renderFrameTop,
22
- } from './frame.ts';
23
- import {
24
- type ElapsedRenderContext,
25
- type ElapsedRenderState,
26
- syncElapsedTicker,
27
- } from './ticker.ts';
28
-
29
- const COLLAPSED_OUTPUT_LINES = 8;
30
- const MAX_ARGS_SUMMARY_LENGTH = 500;
31
- const NESTED_KEYS_SHOWN = 4;
32
-
33
- interface GenericContext extends ElapsedRenderContext {
34
- lastComponent?: { invalidate?: () => void };
35
- state?: ElapsedRenderState & {
36
- hasResult?: boolean;
37
- callComponent?: { invalidate?: () => void };
38
- };
39
- }
40
-
41
- function formatValue(value: unknown): string {
42
- if (typeof value === 'string') return JSON.stringify(value) ?? '""';
43
- try {
44
- return JSON.stringify(value) ?? String(value);
45
- } catch {
46
- return String(value);
47
- }
48
- }
49
-
50
- function summarizeValue(value: unknown, mode: IconMode): string {
51
- if (Array.isArray(value)) return `[${value.length} items]`;
52
- if (value !== null && typeof value === 'object') {
53
- const keys = Object.keys(value);
54
- const shown = keys.slice(0, NESTED_KEYS_SHOWN).join(', ');
55
- return `{${keys.length > NESTED_KEYS_SHOWN ? `${shown}, ${semanticIcon('ellipsis', mode)}` : shown}}`;
56
- }
57
- return formatValue(value);
58
- }
59
-
60
- /** Pretty-print JSON object/array text; anything else is returned unchanged. */
61
- function prettifyJson(text: string): string {
62
- const trimmed = text.trim();
63
- const first = trimmed[0];
64
- if (first !== '{' && first !== '[') return text;
65
- try {
66
- return JSON.stringify(JSON.parse(trimmed), null, 2);
67
- } catch {
68
- return text;
69
- }
70
- }
71
-
72
- /** One-line `key=value` summary; escaping happens at the display boundary. */
73
- export function summarizeArgs(args: unknown, mode: IconMode = 'nerd'): string {
74
- if (args === undefined || args === null) return '';
75
- let summary: string;
76
- if (typeof args === 'object' && !Array.isArray(args)) {
77
- summary = Object.entries(args as Record<string, unknown>)
78
- .map(([key, value]) => `${key}=${summarizeValue(value, mode)}`)
79
- .join(' ');
80
- } else {
81
- summary = formatValue(args);
82
- }
83
- return escapeControlCharacters(summary.slice(0, MAX_ARGS_SUMMARY_LENGTH));
84
- }
85
-
86
- /** Generic frame for tools that provide no renderer of their own. */
87
- export function createGenericTool(
88
- toolName: string,
89
- config: ThemeConfig,
90
- ): ToolRenderers {
91
- const toolIcon = semanticIcon('tool', config.icons);
92
- const name = escapeControlCharacters(toolName);
93
-
94
- return {
95
- renderShell: 'self' as const,
96
- renderCall(rawArgs: unknown, theme: Theme, context: GenericContext) {
97
- syncElapsedTicker(context);
98
- const borderTone = context?.isPartial
99
- ? 'accent'
100
- : getToolBorderTone(context);
101
- const argsSummary = summarizeArgs(rawArgs, config.icons);
102
-
103
- const comp = createComponent((width: number) => {
104
- const footer =
105
- context?.executionStarted && context.isPartial
106
- ? toolFooter(theme, { status: 'running', context }, config.icons)
107
- : undefined;
108
- const title = `${theme.fg('accent', toolIcon)} ${theme.bold ? theme.bold(theme.fg('toolTitle', name)) : theme.fg('toolTitle', name)}`;
109
- const lines = [
110
- ...renderFrameTop(theme, title, width, borderTone),
111
- ...(argsSummary
112
- ? renderFrameRow(
113
- theme,
114
- theme.fg('dim', argsSummary),
115
- width,
116
- borderTone,
117
- )
118
- : []),
119
- ];
120
- if (hasToolResult(context)) return lines;
121
- return [
122
- ...lines,
123
- ...renderFrameBottom(theme, footer, width, borderTone),
124
- ];
125
- });
126
-
127
- if (context?.state) context.state.callComponent = comp;
128
- return comp;
129
- },
130
-
131
- renderResult(
132
- result: AgentToolResult<unknown>,
133
- options: ToolRenderResultOptions,
134
- theme: Theme,
135
- context: GenericContext,
136
- ) {
137
- if (context?.state) {
138
- context.state.hasResult = true;
139
- context.state.callComponent?.invalidate?.();
140
- }
141
- context?.lastComponent?.invalidate?.();
142
-
143
- const isErr = Boolean(context?.isError);
144
- const isPartial = Boolean(options?.isPartial);
145
- const borderTone = isPartial
146
- ? 'accent'
147
- : getToolBorderTone({ isError: isErr, isPartial });
148
- syncElapsedTicker({ ...context, isPartial });
149
- const text = prettifyJson(getResultText(result));
150
- const allLines = text ? text.split('\n') : [];
151
- const lineCount = allLines.length;
152
- const hasImage = hasImageContent(result);
153
-
154
- return createComponent((width: number) => {
155
- const footer = toolFooter(
156
- theme,
157
- {
158
- status: isPartial ? 'running' : isErr ? 'failed' : 'completed',
159
- context: { ...context, isPartial },
160
- summary: `${lineCount} ${lineCount === 1 ? 'line' : 'lines'}`,
161
- },
162
- config.icons,
163
- );
164
-
165
- const visible = options?.expanded
166
- ? allLines
167
- : allLines.slice(0, COLLAPSED_OUTPUT_LINES);
168
- const body = visible.map((line) => {
169
- const sanitized = escapeControlCharacters(line);
170
- return isErr
171
- ? theme.fg('error', sanitized)
172
- : theme.fg('toolOutput', sanitized);
173
- });
174
- if (!options?.expanded && allLines.length > COLLAPSED_OUTPUT_LINES) {
175
- body.push(
176
- theme.fg(
177
- 'dim',
178
- `${semanticIcon('ellipsis', config.icons)} ${allLines.length - COLLAPSED_OUTPUT_LINES} more lines ${semanticIcon('separator', config.icons)} ctrl+o to expand`,
179
- ),
180
- );
181
- }
182
- if (body.length === 0 && !hasImage) {
183
- body.push(theme.fg('dim', '(no output)'));
184
- }
185
-
186
- return [
187
- ...body.flatMap((line) =>
188
- renderFrameRow(theme, line, width, borderTone),
189
- ),
190
- ...renderFrameBottom(theme, footer, width, borderTone),
191
- ];
192
- });
193
- },
194
- };
195
- }