tokenmaw 0.3.0 → 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 +23 -2
- package/agents/coordinator.md +2 -3
- package/agents/main.md +2 -2
- package/dist/backend.js +31 -1
- package/dist/cli.js +30 -0
- package/dist/infra/tools.js +274 -28
- package/dist/markdown.js +83 -48
- package/dist/responses.js +7 -1
- package/dist/runtime/agent-registry.js +35 -5
- package/dist/runtime/agent-runtime.js +352 -21
- package/dist/runtime/agent-store.js +23 -0
- package/dist/runtime/file-lock.js +256 -0
- package/dist/runtime/locks.js +58 -38
- package/dist/runtime/session-timeline.js +32 -3
- package/dist/runtime/workspace-instances.js +109 -0
- package/dist/runtime/worktree.js +321 -0
- package/dist/ui/bracketed-paste.js +231 -0
- package/dist/ui/commands.js +12 -0
- package/dist/ui/fullscreen-tui.js +1195 -120
- package/dist/ui/markdown.js +19 -9
- package/dist/ui/scrollbar.js +370 -0
- package/dist/ui/syntax.js +3 -5
- package/dist/ui/theme.js +198 -0
- package/dist/ui/tui-design.js +78 -0
- package/dist/ui/welcome.js +144 -11
- package/docs/architecture-revision.md +1 -1
- package/package.json +2 -2
package/dist/markdown.js
CHANGED
|
@@ -1,21 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* markdown.ts — Lightweight terminal markdown renderer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Emits Blessed style tags (not raw ANSI) so the TUI renders every highlight
|
|
5
|
+
* with theme-driven colors, and exports the theme shape so unit tests and the
|
|
6
|
+
* TUI share one definition. No external dependencies — pure string transform.
|
|
6
7
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/** Blessed tags quantize hex colors to the terminal palette; chalk's
|
|
9
|
+
* truecolor SGR sequences are dropped by Blessed's attribute parser, which is
|
|
10
|
+
* why highlights previously rendered as plain white. */
|
|
11
|
+
export const DEFAULT_MARKDOWN_THEME = {
|
|
9
12
|
text: '#d7e0ea',
|
|
10
13
|
muted: '#7f92a6',
|
|
11
14
|
accent: '#6fb1d6',
|
|
12
|
-
|
|
15
|
+
heading: '#8ac3e6',
|
|
16
|
+
headingStrong: '#d7e0ea',
|
|
13
17
|
codeBg: '#16212d',
|
|
14
18
|
codeText: '#c7d7e6',
|
|
15
19
|
codeFence: '#5f7388',
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
diffAddBg: '#10281a',
|
|
21
|
+
diffAddText: '#9fd0a6',
|
|
22
|
+
diffDelBg: '#2b1215',
|
|
23
|
+
diffDelText: '#d99f9f',
|
|
18
24
|
};
|
|
25
|
+
let currentTheme = DEFAULT_MARKDOWN_THEME;
|
|
26
|
+
export function setMarkdownTheme(theme) {
|
|
27
|
+
currentTheme = theme;
|
|
28
|
+
}
|
|
29
|
+
export function getMarkdownTheme() {
|
|
30
|
+
return currentTheme;
|
|
31
|
+
}
|
|
32
|
+
/** Blessed reserves braces for style tags; escape literal ones. */
|
|
33
|
+
export function escapeTags(text) {
|
|
34
|
+
return text.replace(/{/g, '{open}').replace(/}/g, '{close}');
|
|
35
|
+
}
|
|
36
|
+
function fg(color, text) {
|
|
37
|
+
return `{${color}-fg}${text}{/${color}-fg}`;
|
|
38
|
+
}
|
|
39
|
+
/** Italic and strikethrough are not Blessed flags; raw SGR is honored by the
|
|
40
|
+
* content parser and excluded from width measurement. */
|
|
41
|
+
function italic(text) {
|
|
42
|
+
return `\x1b[3m${text}\x1b[23m`;
|
|
43
|
+
}
|
|
44
|
+
function strike(text) {
|
|
45
|
+
return `\x1b[9m${text}\x1b[29m`;
|
|
46
|
+
}
|
|
19
47
|
function isDiffLanguage(lang) {
|
|
20
48
|
return lang.toLowerCase() === 'diff' || lang.toLowerCase() === 'patch';
|
|
21
49
|
}
|
|
@@ -31,27 +59,30 @@ export function diffKind(line) {
|
|
|
31
59
|
return 'context';
|
|
32
60
|
}
|
|
33
61
|
export function renderDiffLine(line) {
|
|
62
|
+
const theme = getMarkdownTheme();
|
|
34
63
|
switch (diffKind(line)) {
|
|
35
64
|
case 'add':
|
|
36
|
-
return
|
|
65
|
+
return fg(theme.diffAddText, escapeTags(line));
|
|
37
66
|
case 'del':
|
|
38
|
-
return
|
|
67
|
+
return fg(theme.diffDelText, escapeTags(line));
|
|
39
68
|
case 'hunk':
|
|
40
|
-
return
|
|
69
|
+
return fg(theme.accent, escapeTags(line));
|
|
41
70
|
case 'file':
|
|
42
|
-
return
|
|
71
|
+
return fg(theme.text, escapeTags(line));
|
|
43
72
|
case 'context':
|
|
44
73
|
default:
|
|
45
|
-
return
|
|
74
|
+
return fg(theme.muted, escapeTags(line));
|
|
46
75
|
}
|
|
47
76
|
}
|
|
48
77
|
function renderCodeLine(lang, line) {
|
|
49
|
-
return isDiffLanguage(lang) ? renderDiffLine(line) :
|
|
78
|
+
return isDiffLanguage(lang) ? renderDiffLine(line) : fg(getMarkdownTheme().codeText, escapeTags(line));
|
|
50
79
|
}
|
|
51
80
|
const ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
52
|
-
|
|
81
|
+
const TAG_PATTERN = /\{[^{}]*\}/g;
|
|
82
|
+
/** Display width ignoring ANSI escapes and style tags, counting East-Asian
|
|
83
|
+
* wide chars as 2 columns. */
|
|
53
84
|
export function displayWidth(text) {
|
|
54
|
-
const clean = text.replace(ANSI_PATTERN, '');
|
|
85
|
+
const clean = text.replace(ANSI_PATTERN, '').replace(TAG_PATTERN, '');
|
|
55
86
|
let width = 0;
|
|
56
87
|
for (const ch of clean) {
|
|
57
88
|
const code = ch.codePointAt(0);
|
|
@@ -135,6 +166,7 @@ function truncateToWidth(text, maxWidth) {
|
|
|
135
166
|
}
|
|
136
167
|
/** Render a parsed table as aligned monospace lines that fit within maxWidth columns. */
|
|
137
168
|
export function renderGfmTable(table, maxWidth) {
|
|
169
|
+
const theme = getMarkdownTheme();
|
|
138
170
|
const columns = table.header.length;
|
|
139
171
|
const gap = ' │ ';
|
|
140
172
|
const gapWidth = displayWidth(gap);
|
|
@@ -153,40 +185,43 @@ export function renderGfmTable(table, maxWidth) {
|
|
|
153
185
|
overflow -= reduce;
|
|
154
186
|
}
|
|
155
187
|
}
|
|
156
|
-
const padCell = (
|
|
157
|
-
const padding = Math.max(0, width - displayWidth(
|
|
188
|
+
const padCell = (plain, width, align) => {
|
|
189
|
+
const padding = Math.max(0, width - displayWidth(plain));
|
|
158
190
|
if (align === 'right')
|
|
159
|
-
return ' '.repeat(padding) +
|
|
191
|
+
return ' '.repeat(padding) + plain;
|
|
160
192
|
if (align === 'center') {
|
|
161
193
|
const left = Math.floor(padding / 2);
|
|
162
|
-
return ' '.repeat(left) +
|
|
194
|
+
return ' '.repeat(left) + plain + ' '.repeat(padding - left);
|
|
163
195
|
}
|
|
164
|
-
return
|
|
196
|
+
return plain + ' '.repeat(padding);
|
|
165
197
|
};
|
|
198
|
+
// Pad the plain cell first, then style — so padding never measures tags.
|
|
166
199
|
const renderRow = (cells, style) => cells
|
|
167
200
|
.map((cell, index) => {
|
|
168
201
|
const width = widths[index];
|
|
169
202
|
const plain = displayWidth(cell) > width ? truncateToWidth(cell, width) : cell;
|
|
170
|
-
return padCell(
|
|
203
|
+
return style(padCell(plain, width, table.aligns[index]));
|
|
171
204
|
})
|
|
172
|
-
.join(
|
|
173
|
-
const header = renderRow(table.header, (
|
|
205
|
+
.join(fg(theme.muted, gap));
|
|
206
|
+
const header = renderRow(table.header, (plain) => fg(theme.accent, `{bold}${inlineMarkdown(plain)}{/bold}`));
|
|
174
207
|
// One uniform border color for every structural character (│, ─, ┼).
|
|
175
|
-
const separator =
|
|
176
|
-
const body = table.rows.map((row) => renderRow(row, (
|
|
177
|
-
return [header,
|
|
208
|
+
const separator = fg(theme.muted, widths.map((width) => '─'.repeat(width)).join('─┼─'));
|
|
209
|
+
const body = table.rows.map((row) => renderRow(row, (plain) => inlineMarkdown(plain)));
|
|
210
|
+
return [header, separator, ...body];
|
|
178
211
|
}
|
|
179
212
|
export function inlineMarkdown(text) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.replace(
|
|
183
|
-
.replace(
|
|
184
|
-
.replace(
|
|
185
|
-
.replace(
|
|
186
|
-
.replace(
|
|
187
|
-
.replace(
|
|
213
|
+
const theme = getMarkdownTheme();
|
|
214
|
+
return escapeTags(text)
|
|
215
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, (_m, t) => `{bold}${italic(t)}{/bold}`)
|
|
216
|
+
.replace(/\*\*(.+?)\*\*/g, (_m, t) => `{bold}${t}{/bold}`)
|
|
217
|
+
.replace(/__(.+?)__/g, (_m, t) => `{bold}${t}{/bold}`)
|
|
218
|
+
.replace(/\*(.+?)\*/g, (_m, t) => italic(t))
|
|
219
|
+
.replace(/_(.+?)_/g, (_m, t) => italic(t))
|
|
220
|
+
.replace(/`([^`]+)`/g, (_m, t) => `{${theme.codeBg}-bg}{${theme.codeText}-fg} ${t} {/${theme.codeText}-fg}{/${theme.codeBg}-bg}`)
|
|
221
|
+
.replace(/~~(.+?)~~/g, (_m, t) => strike(t));
|
|
188
222
|
}
|
|
189
223
|
export function renderMarkdown(text, cols = 80) {
|
|
224
|
+
const theme = getMarkdownTheme();
|
|
190
225
|
const lines = text.split('\n');
|
|
191
226
|
const out = [];
|
|
192
227
|
let inCodeBlock = false;
|
|
@@ -203,12 +238,12 @@ export function renderMarkdown(text, cols = 80) {
|
|
|
203
238
|
}
|
|
204
239
|
else {
|
|
205
240
|
inCodeBlock = false;
|
|
206
|
-
const langLabel = codeLang ?
|
|
207
|
-
out.push(
|
|
241
|
+
const langLabel = codeLang ? fg(theme.muted, italic(` ${escapeTags(codeLang)}`)) : '';
|
|
242
|
+
out.push(fg(theme.codeFence, '┌' + '─'.repeat(Math.max(2, cols - 2))) + langLabel);
|
|
208
243
|
for (const cl of codeLines) {
|
|
209
|
-
out.push(
|
|
244
|
+
out.push(fg(theme.codeFence, '│ ') + renderCodeLine(codeLang, cl));
|
|
210
245
|
}
|
|
211
|
-
out.push(
|
|
246
|
+
out.push(fg(theme.codeFence, '└' + '─'.repeat(Math.max(2, cols - 2))));
|
|
212
247
|
codeLang = '';
|
|
213
248
|
codeLines = [];
|
|
214
249
|
}
|
|
@@ -222,19 +257,19 @@ export function renderMarkdown(text, cols = 80) {
|
|
|
222
257
|
const h2 = raw.match(/^## (.+)/);
|
|
223
258
|
const h3 = raw.match(/^### (.+)/);
|
|
224
259
|
if (h1) {
|
|
225
|
-
out.push('\n' +
|
|
260
|
+
out.push('\n' + fg(theme.heading, `{bold}${escapeTags(h1[1])}{/bold}`));
|
|
226
261
|
continue;
|
|
227
262
|
}
|
|
228
263
|
if (h2) {
|
|
229
|
-
out.push('\n' +
|
|
264
|
+
out.push('\n' + fg(theme.headingStrong, `{bold}${escapeTags(h2[1])}{/bold}`));
|
|
230
265
|
continue;
|
|
231
266
|
}
|
|
232
267
|
if (h3) {
|
|
233
|
-
out.push(
|
|
268
|
+
out.push(`{bold}${escapeTags(h3[1])}{/bold}`);
|
|
234
269
|
continue;
|
|
235
270
|
}
|
|
236
271
|
if (/^---+$/.test(raw) || /^\*\*\*+$/.test(raw)) {
|
|
237
|
-
out.push(
|
|
272
|
+
out.push(fg(theme.muted, '─'.repeat(cols)));
|
|
238
273
|
continue;
|
|
239
274
|
}
|
|
240
275
|
const tableMatch = matchGfmTable(lines, index);
|
|
@@ -245,30 +280,30 @@ export function renderMarkdown(text, cols = 80) {
|
|
|
245
280
|
}
|
|
246
281
|
const bullet = raw.match(/^(\s*)[*\-+] (.+)/);
|
|
247
282
|
if (bullet) {
|
|
248
|
-
out.push((bullet[1] ?? '') +
|
|
283
|
+
out.push((bullet[1] ?? '') + fg(theme.accent, '•') + ' ' + inlineMarkdown(bullet[2] ?? ''));
|
|
249
284
|
continue;
|
|
250
285
|
}
|
|
251
286
|
const numbered = raw.match(/^(\s*)(\d+)\. (.+)/);
|
|
252
287
|
if (numbered) {
|
|
253
288
|
out.push((numbered[1] ?? '') +
|
|
254
|
-
|
|
289
|
+
fg(theme.accent, escapeTags(numbered[2] + '.')) +
|
|
255
290
|
' ' +
|
|
256
291
|
inlineMarkdown(numbered[3] ?? ''));
|
|
257
292
|
continue;
|
|
258
293
|
}
|
|
259
294
|
const bq = raw.match(/^> (.+)/);
|
|
260
295
|
if (bq) {
|
|
261
|
-
out.push(
|
|
296
|
+
out.push(fg(theme.muted, '│ ') + fg(theme.muted, italic(escapeTags(bq[1]))));
|
|
262
297
|
continue;
|
|
263
298
|
}
|
|
264
299
|
out.push(inlineMarkdown(raw));
|
|
265
300
|
}
|
|
266
301
|
if (inCodeBlock && codeLines.length > 0) {
|
|
267
|
-
out.push(
|
|
302
|
+
out.push(fg(theme.codeFence, '┌─'));
|
|
268
303
|
for (const cl of codeLines) {
|
|
269
|
-
out.push(
|
|
304
|
+
out.push(fg(theme.codeFence, '│ ') + renderCodeLine(codeLang, cl));
|
|
270
305
|
}
|
|
271
|
-
out.push(
|
|
306
|
+
out.push(fg(theme.codeFence, '└─'));
|
|
272
307
|
}
|
|
273
308
|
return out.join('\n');
|
|
274
309
|
}
|
package/dist/responses.js
CHANGED
|
@@ -66,7 +66,13 @@ export async function* responsesStream(config, instructions, messages, tools, si
|
|
|
66
66
|
throw new Error(event.response?.error?.message ?? event.message ?? event.response?.incomplete_details?.reason ?? `Responses: ${event.type}`);
|
|
67
67
|
}
|
|
68
68
|
if (event.type === 'response.completed') {
|
|
69
|
-
|
|
69
|
+
const usage = event.response?.usage;
|
|
70
|
+
yield { content: null, done: true, usage: usage ? {
|
|
71
|
+
inputTokens: usage.input_tokens,
|
|
72
|
+
outputTokens: usage.output_tokens,
|
|
73
|
+
cachedInputTokens: usage.input_tokens_details?.cached_tokens,
|
|
74
|
+
reasoningTokens: usage.output_tokens_details?.reasoning_tokens,
|
|
75
|
+
} : undefined };
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
function unquote(value) {
|
|
5
5
|
const trimmed = value.trim();
|
|
6
6
|
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
@@ -86,6 +86,25 @@ async function markdownFiles(root) {
|
|
|
86
86
|
function specId(root, file) {
|
|
87
87
|
return relative(root, file).split(sep).join('/').replace(/\.md$/i, '');
|
|
88
88
|
}
|
|
89
|
+
/** Names checked in order when loading project context. The lowercase form
|
|
90
|
+
* covers case-insensitive filesystems; the capitalized forms follow the
|
|
91
|
+
* agents.md convention used by other coding agents. */
|
|
92
|
+
const WORKSPACE_CONTEXT_FILENAMES = ['AGENTS.md', 'AGENT.md', 'agents.md'];
|
|
93
|
+
/** Loads an optional AGENTS.md-style project context document from the
|
|
94
|
+
* workspace root. Plain Markdown with no frontmatter; a missing, empty, or
|
|
95
|
+
* unreadable file is not an error — the convention is opt-in per workspace. */
|
|
96
|
+
export async function loadWorkspaceContext(root) {
|
|
97
|
+
for (const name of WORKSPACE_CONTEXT_FILENAMES) {
|
|
98
|
+
try {
|
|
99
|
+
const content = (await readFile(join(root, name), 'utf8')).replace(/^\uFEFF/, '').trim();
|
|
100
|
+
if (content)
|
|
101
|
+
return content;
|
|
102
|
+
// An empty file counts as absent; keep looking at the remaining names.
|
|
103
|
+
}
|
|
104
|
+
catch { /* try the next candidate name */ }
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
89
108
|
function matchesSelector(id, selector) {
|
|
90
109
|
if (selector === '*')
|
|
91
110
|
return true;
|
|
@@ -96,15 +115,26 @@ function matchesSelector(id, selector) {
|
|
|
96
115
|
export class AgentRegistry {
|
|
97
116
|
specs = new Map();
|
|
98
117
|
roots;
|
|
118
|
+
builtinDir;
|
|
119
|
+
userDir;
|
|
120
|
+
projectDir;
|
|
99
121
|
constructor(options = {}) {
|
|
100
122
|
const workspaceRoot = resolve(options.workspaceRoot ?? process.cwd());
|
|
101
|
-
|
|
123
|
+
this.builtinDir = resolve(options.builtinDir ?? resolve(import.meta.dirname, '..', '..', 'agents'));
|
|
124
|
+
this.userDir = resolve(options.userDir ?? resolve(homedir(), '.coder', 'agents'));
|
|
125
|
+
this.projectDir = options.projectDir ? resolve(options.projectDir) : undefined;
|
|
102
126
|
this.roots = [
|
|
103
|
-
{ path: builtinDir, scope: 'builtin' },
|
|
104
|
-
{ path:
|
|
105
|
-
{ path:
|
|
127
|
+
{ path: this.builtinDir, scope: 'builtin' },
|
|
128
|
+
{ path: this.userDir, scope: 'user' },
|
|
129
|
+
{ path: this.projectDir ?? resolve(workspaceRoot, '.coder', 'agents'), scope: 'project' },
|
|
106
130
|
];
|
|
107
131
|
}
|
|
132
|
+
/** Point the project-scope spec root at another workspace (<root>/.coder/agents).
|
|
133
|
+
* Takes effect on the next load(); used by /cd when switching workspaces. */
|
|
134
|
+
setProjectDir(workspaceRoot) {
|
|
135
|
+
this.projectDir = resolve(workspaceRoot, '.coder', 'agents');
|
|
136
|
+
this.roots[this.roots.length - 1] = { path: this.projectDir, scope: 'project' };
|
|
137
|
+
}
|
|
108
138
|
async load() {
|
|
109
139
|
this.specs.clear();
|
|
110
140
|
for (const root of this.roots) {
|