klyro 0.1.11 → 0.1.14
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/dist/agent/provider-adapter.js +8 -2
- package/dist/agent/registry.js +39 -9
- package/dist/agent/runtime.d.ts +6 -1
- package/dist/agent/runtime.js +63 -2
- package/dist/chat.js +13 -4
- package/dist/cli/eval.d.ts +5 -0
- package/dist/cli/eval.js +61 -0
- package/dist/cli/repl.js +39 -10
- package/dist/eval/harness.d.ts +15 -0
- package/dist/eval/harness.js +73 -2
- package/dist/index.js +28 -3
- package/dist/tools/plan/ask-user.d.ts +12 -0
- package/dist/tools/plan/ask-user.js +28 -0
- package/dist/tools/plan/todo-write.d.ts +10 -0
- package/dist/tools/plan/todo-write.js +31 -0
- package/dist/tools/registry.js +4 -0
- package/dist/tui/app.js +33 -2
- package/package.json +1 -1
|
@@ -211,8 +211,14 @@ async function* streamChatCompletions(url, opts, req, fetchImpl) {
|
|
|
211
211
|
pendingUsage = { input: chunk.usage.prompt_tokens, output: chunk.usage.completion_tokens };
|
|
212
212
|
}
|
|
213
213
|
for (const choice of chunk.choices) {
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
// Handle multiple possible content fields for compat (OpenAI, Ollama, OpenRouter gemini, etc.)
|
|
215
|
+
const delta = choice.delta;
|
|
216
|
+
const text = delta?.content ??
|
|
217
|
+
delta?.text ??
|
|
218
|
+
choice?.message?.content ??
|
|
219
|
+
choice?.delta?.text;
|
|
220
|
+
if (typeof text === 'string' && text) {
|
|
221
|
+
yield { kind: 'text_delta', text };
|
|
216
222
|
}
|
|
217
223
|
for (const tc of choice.delta.tool_calls ?? []) {
|
|
218
224
|
if (tc.id && tc.function?.name) {
|
package/dist/agent/registry.js
CHANGED
|
@@ -47,20 +47,46 @@ function isLoopbackBaseURL(url) {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
const PROVIDER_ALIASES = {
|
|
51
|
+
'9router': 'openai',
|
|
52
|
+
'openrouter': 'openai',
|
|
53
|
+
'groq': 'openai',
|
|
54
|
+
'ollama': 'openai',
|
|
55
|
+
'vllm': 'openai',
|
|
56
|
+
'lmstudio': 'openai',
|
|
57
|
+
};
|
|
58
|
+
function normalizeProviderName(name) {
|
|
59
|
+
if (!name)
|
|
60
|
+
return undefined;
|
|
61
|
+
const lower = name.toLowerCase().trim();
|
|
62
|
+
if (lower === 'openai' || lower === 'anthropic')
|
|
63
|
+
return lower;
|
|
64
|
+
return PROVIDER_ALIASES[lower];
|
|
65
|
+
}
|
|
50
66
|
export function buildProvider(opts = {}) {
|
|
51
67
|
const baseURL = opts.baseURL ?? process.env.KLYRO_BASE_URL;
|
|
52
68
|
const apiKey = opts.apiKey ?? process.env.KLYRO_API_KEY;
|
|
53
69
|
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
54
|
-
// Resolve provider
|
|
70
|
+
// Resolve provider (with aliases like 9router -> openrouter -> openai)
|
|
55
71
|
let provider;
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
const normalizedOpt = normalizeProviderName(opts.provider);
|
|
73
|
+
if (normalizedOpt) {
|
|
74
|
+
provider = normalizedOpt;
|
|
58
75
|
}
|
|
59
|
-
else if (
|
|
60
|
-
|
|
76
|
+
else if (opts.provider) {
|
|
77
|
+
// Fallback: treat any unknown as openai (OpenAI-compatible)
|
|
78
|
+
provider = 'openai';
|
|
61
79
|
}
|
|
62
80
|
else {
|
|
63
|
-
|
|
81
|
+
const envProvider = normalizeProviderName(process.env.KLYRO_PROVIDER);
|
|
82
|
+
if (envProvider)
|
|
83
|
+
provider = envProvider;
|
|
84
|
+
else if (process.env.KLYRO_PROVIDER) {
|
|
85
|
+
// Unknown provider string -> treat as openai
|
|
86
|
+
provider = 'openai';
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
provider = inferProviderFromBaseURL(baseURL);
|
|
64
90
|
}
|
|
65
91
|
let inner;
|
|
66
92
|
if (provider === 'anthropic') {
|
|
@@ -90,9 +116,13 @@ export function buildProvider(opts = {}) {
|
|
|
90
116
|
* This is the version called from CLI entry points.
|
|
91
117
|
*/
|
|
92
118
|
export function buildProviderFromCli(args) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
const raw = args.provider?.toLowerCase().trim();
|
|
120
|
+
const normalized = raw ? normalizeProviderName(raw) ?? (raw ? 'openai' : undefined) : undefined;
|
|
121
|
+
const provider = normalized;
|
|
122
|
+
// No longer throws for unknown — treat as openai (e.g. 9router, groq)
|
|
123
|
+
if (raw && !normalized && raw !== 'openai' && raw !== 'anthropic') {
|
|
124
|
+
// Silently treat as openai, but log hint
|
|
125
|
+
process.stderr.write(`klyro: unknown provider "${raw}" — treating as openai-compatible\n`);
|
|
96
126
|
}
|
|
97
127
|
return buildProvider({
|
|
98
128
|
provider,
|
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface RuntimeDeps {
|
|
|
35
35
|
telemetry?: string;
|
|
36
36
|
}) => string;
|
|
37
37
|
}
|
|
38
|
+
export type Phase = 'understanding' | 'exploring' | 'planning' | 'implementing' | 'verifying' | 'done' | 'blocked' | 'limit';
|
|
38
39
|
export interface RunOptions {
|
|
39
40
|
task: string;
|
|
40
41
|
cwd: string;
|
|
@@ -42,6 +43,8 @@ export interface RunOptions {
|
|
|
42
43
|
maxSteps?: number;
|
|
43
44
|
/** Alias for maxSteps (3.5) */
|
|
44
45
|
maxTurns?: number;
|
|
46
|
+
maxCost?: number;
|
|
47
|
+
maxTimeMs?: number;
|
|
45
48
|
maxTokens?: number;
|
|
46
49
|
temperature?: number;
|
|
47
50
|
signal?: AbortSignal;
|
|
@@ -164,7 +167,7 @@ export type RuntimeEvent = {
|
|
|
164
167
|
sessionId: string;
|
|
165
168
|
};
|
|
166
169
|
export interface RunResult {
|
|
167
|
-
status: 'complete' | 'max_steps' | 'aborted' | 'no_final' | 'verify_failed';
|
|
170
|
+
status: 'complete' | 'max_steps' | 'aborted' | 'no_final' | 'verify_failed' | 'limit' | 'blocked';
|
|
168
171
|
steps: number;
|
|
169
172
|
toolCalls: number;
|
|
170
173
|
finalText: string;
|
|
@@ -182,6 +185,8 @@ export interface RunResult {
|
|
|
182
185
|
attempts: number;
|
|
183
186
|
failureType?: string;
|
|
184
187
|
};
|
|
188
|
+
/** 5.1 phase */
|
|
189
|
+
phase?: Phase;
|
|
185
190
|
}
|
|
186
191
|
/** Convert a registry of tools into ToolDefinitions for the provider. */
|
|
187
192
|
export declare function toolDefinitions(registry: ToolRegistry): ToolDefinition[];
|
package/dist/agent/runtime.js
CHANGED
|
@@ -77,7 +77,19 @@ export async function run(opts, deps) {
|
|
|
77
77
|
}
|
|
78
78
|
catch { /* ignore */ }
|
|
79
79
|
};
|
|
80
|
-
//
|
|
80
|
+
// 5.1 — phases and limits
|
|
81
|
+
const maxCost = opts.maxCost;
|
|
82
|
+
const maxTimeMs = opts.maxTimeMs;
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
let phase = 'understanding';
|
|
85
|
+
const setPhase = (p) => {
|
|
86
|
+
if (p !== phase) {
|
|
87
|
+
phase = p;
|
|
88
|
+
// Emit as any (RuntimeEvent extension) + KlyroEvent
|
|
89
|
+
emit?.({ kind: 'phase_changed', phase });
|
|
90
|
+
emitKlyro({ type: 'phase.changed', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', phase });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
81
93
|
const store = opts.persist?.store;
|
|
82
94
|
const sessionId = opts.persist?.sessionId;
|
|
83
95
|
async function checkpoint(msg, obs) {
|
|
@@ -109,7 +121,26 @@ export async function run(opts, deps) {
|
|
|
109
121
|
// Fire-and-forget initial checkpoint (don't await to block loop start)
|
|
110
122
|
void checkpoint(transcript[transcript.length - 1]);
|
|
111
123
|
}
|
|
124
|
+
// 5.2 — stuck detection state
|
|
125
|
+
const callHistory = [];
|
|
126
|
+
const fileEditCounts = new Map();
|
|
127
|
+
let stuckCount = 0;
|
|
128
|
+
let lastSignal;
|
|
112
129
|
outer: while (steps < maxSteps) {
|
|
130
|
+
// 5.1 limits: max-cost, max-time
|
|
131
|
+
if (maxCost !== undefined) {
|
|
132
|
+
const cost = (usage.input / 1000) * 0.003 + (usage.output / 1000) * 0.015;
|
|
133
|
+
if (cost >= maxCost) {
|
|
134
|
+
setPhase('limit');
|
|
135
|
+
await closeTracer();
|
|
136
|
+
return { status: 'limit', steps, toolCalls: toolCallCount, finalText: `Stopped: max cost $${maxCost} reached (cost $${cost.toFixed(2)})`, transcript, usage, repairs, phase: 'limit' };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (maxTimeMs !== undefined && Date.now() - startTime >= maxTimeMs) {
|
|
140
|
+
setPhase('limit');
|
|
141
|
+
await closeTracer();
|
|
142
|
+
return { status: 'limit', steps, toolCalls: toolCallCount, finalText: `Stopped: max time ${maxTimeMs}ms reached`, transcript, usage, repairs, phase: 'limit' };
|
|
143
|
+
}
|
|
113
144
|
if (opts.signal?.aborted) {
|
|
114
145
|
emit?.({ kind: 'aborted' });
|
|
115
146
|
if (store && sessionId) {
|
|
@@ -119,9 +150,20 @@ export async function run(opts, deps) {
|
|
|
119
150
|
catch { /* ignore */ }
|
|
120
151
|
}
|
|
121
152
|
await closeTracer();
|
|
122
|
-
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs, verification: hasEdits ? { ok: false, attempts: verificationAttempts } : undefined };
|
|
153
|
+
return { status: 'aborted', steps, toolCalls: toolCallCount, finalText, transcript, usage, repairs, verification: hasEdits ? { ok: false, attempts: verificationAttempts } : undefined, phase: 'blocked' };
|
|
123
154
|
}
|
|
124
155
|
steps++;
|
|
156
|
+
// 5.1 phase transitions (model-narrated)
|
|
157
|
+
if (steps === 1)
|
|
158
|
+
setPhase('understanding');
|
|
159
|
+
else if (steps === 2)
|
|
160
|
+
setPhase('exploring');
|
|
161
|
+
else if (steps === 3)
|
|
162
|
+
setPhase('planning');
|
|
163
|
+
else if (hasEdits)
|
|
164
|
+
setPhase('implementing');
|
|
165
|
+
else if (steps > 3)
|
|
166
|
+
setPhase('verifying');
|
|
125
167
|
emit?.({ kind: 'step_start', step: steps });
|
|
126
168
|
telemetry.recordStepStart(steps);
|
|
127
169
|
const req = {
|
|
@@ -377,8 +419,27 @@ export async function run(opts, deps) {
|
|
|
377
419
|
await snapshot(opts.cwd, [fileChanged.path]);
|
|
378
420
|
}
|
|
379
421
|
catch { /* ignore */ }
|
|
422
|
+
// 5.2 file edit count
|
|
423
|
+
const cnt = (fileEditCounts.get(fileChanged.path) ?? 0) + 1;
|
|
424
|
+
fileEditCounts.set(fileChanged.path, cnt);
|
|
425
|
+
if (cnt > 8) {
|
|
426
|
+
emitKlyro({ type: 'error', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', code: 'stuck', message: `same file edited >8×: ${fileChanged.path}` });
|
|
427
|
+
}
|
|
380
428
|
}
|
|
381
429
|
}
|
|
430
|
+
// 5.2 identical call ×3
|
|
431
|
+
const sig = `${call.name}:${JSON.stringify(call.input).slice(0, 200)}`;
|
|
432
|
+
callHistory.push(sig);
|
|
433
|
+
if (callHistory.length > 10)
|
|
434
|
+
callHistory.shift();
|
|
435
|
+
const last3 = callHistory.slice(-3);
|
|
436
|
+
if (last3.length === 3 && last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
437
|
+
emitKlyro({ type: 'error', ts: Date.now(), sessionId: sessionId ?? 'ephemeral', code: 'stuck', message: `identical call ×3: ${sig}` });
|
|
438
|
+
// Inject system note for next turn
|
|
439
|
+
const note = { role: 'user', content: [text(`[system note] Stuck detected: identical call ×3: ${sig}. Try a different approach.`)] };
|
|
440
|
+
transcript.push(note);
|
|
441
|
+
await checkpoint(note);
|
|
442
|
+
}
|
|
382
443
|
};
|
|
383
444
|
// 3.5 — parallel if all concurrencySafe, sequential otherwise
|
|
384
445
|
if (allSafe) {
|
package/dist/chat.js
CHANGED
|
@@ -33,19 +33,28 @@ export function assertSafeBaseURL(url) {
|
|
|
33
33
|
if (parsed.protocol === 'https:')
|
|
34
34
|
return;
|
|
35
35
|
if (parsed.protocol === 'http:') {
|
|
36
|
+
// Allow insecure HTTP if explicitly opted in (for remote Ollama etc.)
|
|
37
|
+
if (process.env.KLYRO_ALLOW_INSECURE === '1')
|
|
38
|
+
return;
|
|
36
39
|
const host = parsed.hostname.toLowerCase();
|
|
37
|
-
// Allow loopback
|
|
38
|
-
// Note: hostnames that resolve to loopback (e.g. nip.io) still require https — we check hostname, not DNS.
|
|
40
|
+
// Allow loopback and private networks without extra flag
|
|
39
41
|
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0' || host === '::' || host === '[::]')
|
|
40
42
|
return;
|
|
41
|
-
// Also allow 127.0.0.0/8 range via prefix check (e.g. 127.0.0.2)
|
|
42
43
|
if (host.startsWith('127.')) {
|
|
43
44
|
const parts = host.split('.');
|
|
44
45
|
if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255))
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
48
|
+
// Private ranges 10/8, 192.168/16, 172.16-31/12
|
|
49
|
+
if (/^10\.\d+\.\d+\.\d+$/.test(host))
|
|
50
|
+
return;
|
|
51
|
+
if (/^192\.168\.\d+\.\d+$/.test(host))
|
|
52
|
+
return;
|
|
53
|
+
if (/^172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+$/.test(host))
|
|
54
|
+
return;
|
|
47
55
|
throw new Error(`Refusing to send KLYRO_API_KEY over plaintext HTTP to ${host}. ` +
|
|
48
|
-
`Use https:// or a localhost URL (
|
|
56
|
+
`Use https:// or a localhost/private URL, or set KLYRO_ALLOW_INSECURE=1 to allow insecure HTTP (not recommended). ` +
|
|
57
|
+
`Example: $Env:KLYRO_ALLOW_INSECURE=\"1\"; klyro`);
|
|
49
58
|
}
|
|
50
59
|
throw new Error(`Unsupported KLYRO_BASE_URL protocol: ${parsed.protocol}`);
|
|
51
60
|
}
|
package/dist/cli/eval.d.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface EvalResult {
|
|
|
69
69
|
export interface RunEvalOptions {
|
|
70
70
|
inputPath: string;
|
|
71
71
|
output: 'human' | 'json' | 'silent';
|
|
72
|
+
suite?: string;
|
|
73
|
+
filter?: string;
|
|
74
|
+
runs?: number;
|
|
75
|
+
parallel?: number;
|
|
76
|
+
model?: string;
|
|
72
77
|
}
|
|
73
78
|
export declare function runEval(opts: RunEvalOptions): Promise<number>;
|
|
74
79
|
export declare function scriptedAdapterFromSpec(spec: Array<Array<unknown[]>> | undefined): ProviderAdapter;
|
package/dist/cli/eval.js
CHANGED
|
@@ -47,6 +47,67 @@ import { builtinRegistry } from '../tools/registry.js';
|
|
|
47
47
|
import { builtinRules, DEFAULT_POLICY_CONFIG, PolicyEngine } from '../policy/engine.js';
|
|
48
48
|
import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
49
49
|
export async function runEval(opts) {
|
|
50
|
+
// 5.4 — suite mode: load from evals/fixtures
|
|
51
|
+
if (opts.suite) {
|
|
52
|
+
const { runHarness, loadFileFixture } = await import('../eval/harness.js');
|
|
53
|
+
const fs = await import('node:fs/promises');
|
|
54
|
+
const path = await import('node:path');
|
|
55
|
+
const suiteDir = path.join(process.cwd(), 'evals', 'fixtures', opts.suite === 'smoke' ? '' : opts.suite);
|
|
56
|
+
// For smoke, use the 10 fixtures directly
|
|
57
|
+
const fixturesDir = path.join(process.cwd(), 'evals', 'fixtures');
|
|
58
|
+
let tasks = [];
|
|
59
|
+
try {
|
|
60
|
+
const entries = await fs.readdir(fixturesDir);
|
|
61
|
+
for (const e of entries) {
|
|
62
|
+
if (opts.filter && !e.includes(opts.filter))
|
|
63
|
+
continue;
|
|
64
|
+
const taskPath = path.join(fixturesDir, e, 'task.md');
|
|
65
|
+
try {
|
|
66
|
+
const task = await fs.readFile(taskPath, 'utf-8');
|
|
67
|
+
tasks.push({
|
|
68
|
+
id: e,
|
|
69
|
+
description: e,
|
|
70
|
+
task: task.trim(),
|
|
71
|
+
script: [],
|
|
72
|
+
expectStatus: 'complete',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* no fixtures */ }
|
|
79
|
+
if (tasks.length === 0) {
|
|
80
|
+
stderr.write(`klyro eval: no fixtures for suite ${opts.suite}\n`);
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
// Simple harness for suite: just run check.sh via file fixtures
|
|
84
|
+
const { runFileFixture, loadFileFixture: loadFF } = await import('../eval/harness.js');
|
|
85
|
+
const results = [];
|
|
86
|
+
for (const t of tasks) {
|
|
87
|
+
const fixture = await loadFF(path.join(fixturesDir, t.id));
|
|
88
|
+
const r = await runFileFixture(fixture, { runs: opts.runs, parallel: opts.parallel });
|
|
89
|
+
results.push(r);
|
|
90
|
+
const tag = r.status === 'pass' ? 'PASS' : 'FAIL';
|
|
91
|
+
if (opts.output === 'json')
|
|
92
|
+
stdout.write(JSON.stringify({ kind: 'eval_result', ...r }) + '\n');
|
|
93
|
+
else
|
|
94
|
+
stdout.write(`[${tag}] ${r.id}\n`);
|
|
95
|
+
}
|
|
96
|
+
const passed = results.filter((r) => r.status === 'pass').length;
|
|
97
|
+
if (opts.output === 'json')
|
|
98
|
+
stdout.write(JSON.stringify({ kind: 'eval_summary', total: results.length, passed, failed: results.length - passed }) + '\n');
|
|
99
|
+
else
|
|
100
|
+
stdout.write(`\n${passed}/${results.length} passed\n`);
|
|
101
|
+
// Save results
|
|
102
|
+
try {
|
|
103
|
+
const outDir = path.join(process.cwd(), 'evals', 'results');
|
|
104
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
105
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
106
|
+
await fs.writeFile(path.join(outDir, `${ts}.json`), JSON.stringify({ suite: opts.suite, results }, null, 2));
|
|
107
|
+
}
|
|
108
|
+
catch { /* ignore */ }
|
|
109
|
+
return passed === results.length ? 0 : 1;
|
|
110
|
+
}
|
|
50
111
|
const scenarios = await readScenarios(opts.inputPath);
|
|
51
112
|
if (scenarios.length === 0) {
|
|
52
113
|
stderr.write('klyro eval: no scenarios in input\n');
|
package/dist/cli/repl.js
CHANGED
|
@@ -189,8 +189,16 @@ export async function startRepl(opts = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
else if (ev.kind === 'text_delta') {
|
|
191
191
|
textBuf += ev.text;
|
|
192
|
-
//
|
|
193
|
-
|
|
192
|
+
// Try full-screen live region first (batched 30fps)
|
|
193
|
+
const g = globalThis;
|
|
194
|
+
if (isMounted && g.__klyroAppendDelta) {
|
|
195
|
+
g.__klyroAppendDelta(ev.text);
|
|
196
|
+
// Also keep coalescing for fallback inline mode
|
|
197
|
+
if (!pendingTextId)
|
|
198
|
+
pendingTextId = `text-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Fallback inline mode: coalesce via queuedAppend
|
|
194
202
|
if (pendingTextId) {
|
|
195
203
|
const last = pendingQueue[pendingQueue.length - 1];
|
|
196
204
|
if (last?.kind === 'append' && last.item.kind === 'text' && last.item.id === pendingTextId) {
|
|
@@ -198,8 +206,6 @@ export async function startRepl(opts = {}) {
|
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
|
-
// For mounted case, App will merge via its own coalescing (same id)
|
|
202
|
-
// so reuse pendingTextId to let App merge
|
|
203
209
|
if (pendingTextId && isMounted) {
|
|
204
210
|
queuedAppend({ id: pendingTextId, kind: 'text', text: ev.text, role: 'assistant' });
|
|
205
211
|
return;
|
|
@@ -265,6 +271,18 @@ export async function startRepl(opts = {}) {
|
|
|
265
271
|
status: ev.isError ? 'error' : 'done',
|
|
266
272
|
});
|
|
267
273
|
}
|
|
274
|
+
else if (ev.kind === 'final_text') {
|
|
275
|
+
// Commit live region for both inline and full-screen TUI
|
|
276
|
+
const g2 = globalThis;
|
|
277
|
+
g2.__klyroCommitLive?.();
|
|
278
|
+
pendingTextId = null;
|
|
279
|
+
// For full-screen, also ensure liveText is committed if any remaining
|
|
280
|
+
if (ev.text) {
|
|
281
|
+
const g3 = globalThis;
|
|
282
|
+
// If liveText was batched, ensure it's flushed and committed
|
|
283
|
+
g2.__klyroCommitLive?.();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
268
286
|
else if (ev.kind === 'usage') {
|
|
269
287
|
queuedStatus({ usageInput: ev.input, usageOutput: ev.output });
|
|
270
288
|
}
|
|
@@ -307,9 +325,23 @@ export async function startRepl(opts = {}) {
|
|
|
307
325
|
queuedAppend({ id: `verr-${Date.now()}`, kind: 'error', message: `Verification failed after ${result.verification?.attempts ?? 3} attempts. Check output above.` });
|
|
308
326
|
queuedStatus({ status: 'error', errorMessage: 'verification failed' });
|
|
309
327
|
}
|
|
310
|
-
else if (result.status === 'no_final'
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
else if (result.status === 'no_final') {
|
|
329
|
+
if (result.finalText) {
|
|
330
|
+
queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
// For simple chat, no_final with empty text is often a provider quirk (e.g. gemini via openai compat)
|
|
334
|
+
// Show a helpful message but don't mark as error in header for chat
|
|
335
|
+
const isSimpleChat = taskText.trim().split(/\s+/).length <= 6;
|
|
336
|
+
if (isSimpleChat) {
|
|
337
|
+
queuedStatus({ status: 'done', repairs: result.repairs ?? 0 });
|
|
338
|
+
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'text', text: `(no response — try /model ${model} or check provider logs)`, role: 'assistant' });
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
queuedStatus({ status: 'error', errorMessage: 'no final text' });
|
|
342
|
+
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'error', message: 'Provider returned no final text — check model/provider (try /model or /doctor)' });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
313
345
|
}
|
|
314
346
|
else {
|
|
315
347
|
queuedStatus({ status: 'complete' === result.status ? 'done' : 'error', repairs: result.repairs ?? 0 });
|
|
@@ -317,9 +349,6 @@ export async function startRepl(opts = {}) {
|
|
|
317
349
|
if (sessionId) {
|
|
318
350
|
queuedAppend({ id: `sess-end-${Date.now()}`, kind: 'text', text: `session ${sessionId.slice(0, 8)} ${result.status} — ${result.steps} steps, ${result.toolCalls} tool calls`, role: 'assistant' });
|
|
319
351
|
}
|
|
320
|
-
else if (result.status === 'no_final' && !result.finalText) {
|
|
321
|
-
queuedAppend({ id: `no_final-${Date.now()}`, kind: 'error', message: 'Provider returned no final text — check model/provider' });
|
|
322
|
-
}
|
|
323
352
|
}
|
|
324
353
|
catch (err) {
|
|
325
354
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/eval/harness.d.ts
CHANGED
|
@@ -41,8 +41,23 @@ export interface HarnessSummary {
|
|
|
41
41
|
export declare function runHarness(tasks: ScriptedTask[]): Promise<HarnessSummary>;
|
|
42
42
|
/** Format a harness summary as a markdown report. */
|
|
43
43
|
export declare function formatReport(summary: HarnessSummary): string;
|
|
44
|
+
/** 5.4 — File-based fixture support: repo|repo.json, task.md, check.sh, meta.json */
|
|
45
|
+
export interface FileFixture {
|
|
46
|
+
dir: string;
|
|
47
|
+
task: string;
|
|
48
|
+
checkSh: string;
|
|
49
|
+
meta: Record<string, unknown>;
|
|
50
|
+
repo?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function loadFileFixture(dir: string): Promise<FileFixture>;
|
|
53
|
+
export declare function runFileFixture(fixture: FileFixture, opts?: {
|
|
54
|
+
runs?: number;
|
|
55
|
+
parallel?: number;
|
|
56
|
+
}): Promise<TaskResult>;
|
|
44
57
|
/** Hook the harness up to a session + audit log so task runs are durable. */
|
|
45
58
|
export declare function runHarnessWithPersistence(tasks: ScriptedTask[], opts: {
|
|
46
59
|
storeDir: string;
|
|
47
60
|
auditPath: string;
|
|
48
61
|
}): Promise<HarnessSummary>;
|
|
62
|
+
/** Compare two harness runs */
|
|
63
|
+
export declare function compareReports(a: HarnessSummary, b: HarnessSummary): string;
|
package/dist/eval/harness.js
CHANGED
|
@@ -117,6 +117,57 @@ export function formatReport(summary) {
|
|
|
117
117
|
];
|
|
118
118
|
return lines.join('\n');
|
|
119
119
|
}
|
|
120
|
+
export async function loadFileFixture(dir) {
|
|
121
|
+
const task = await fs.readFile(path.join(dir, 'task.md'), 'utf-8').catch(() => 'test task');
|
|
122
|
+
const checkSh = await fs.readFile(path.join(dir, 'check.sh'), 'utf-8').catch(() => 'exit 0');
|
|
123
|
+
let meta = {};
|
|
124
|
+
try {
|
|
125
|
+
meta = JSON.parse(await fs.readFile(path.join(dir, 'meta.json'), 'utf-8'));
|
|
126
|
+
}
|
|
127
|
+
catch { /* ignore */ }
|
|
128
|
+
let repo;
|
|
129
|
+
try {
|
|
130
|
+
repo = await fs.readFile(path.join(dir, 'repo'), 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
catch { /* ignore */ }
|
|
133
|
+
return { dir, task: task.trim(), checkSh, meta, repo };
|
|
134
|
+
}
|
|
135
|
+
export async function runFileFixture(fixture, opts = {}) {
|
|
136
|
+
const start = Date.now();
|
|
137
|
+
const tmp = path.join(os.tmpdir(), 'klyro-eval-file-' + Math.random().toString(36).slice(2));
|
|
138
|
+
await fs.mkdir(tmp, { recursive: true });
|
|
139
|
+
// Copy repo if exists
|
|
140
|
+
if (fixture.repo) {
|
|
141
|
+
const src = path.isAbsolute(fixture.repo) ? fixture.repo : path.join(fixture.dir, fixture.repo);
|
|
142
|
+
await fs.cp(src, tmp, { recursive: true }).catch(() => undefined);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
await fs.cp(fixture.dir, tmp, { recursive: true }).catch(() => undefined);
|
|
146
|
+
}
|
|
147
|
+
// Run check.sh via shell
|
|
148
|
+
const { spawn } = await import('node:child_process');
|
|
149
|
+
const result = await new Promise((resolve) => {
|
|
150
|
+
const child = spawn('bash', ['-c', fixture.checkSh], { cwd: tmp, shell: false });
|
|
151
|
+
let out = '';
|
|
152
|
+
child.stdout?.on('data', (b) => { out += b.toString(); });
|
|
153
|
+
child.stderr?.on('data', (b) => { out += b.toString(); });
|
|
154
|
+
child.on('close', (code) => {
|
|
155
|
+
const pass = code === 0;
|
|
156
|
+
resolve({
|
|
157
|
+
id: path.basename(fixture.dir),
|
|
158
|
+
status: pass ? 'pass' : 'fail',
|
|
159
|
+
details: out.slice(0, 500),
|
|
160
|
+
observedStatus: pass ? 'complete' : 'verify_failed',
|
|
161
|
+
durationMs: Date.now() - start,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
resolve({ id: path.basename(fixture.dir), status: 'fail', details: String(err), durationMs: Date.now() - start });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
await fs.rm(tmp, { recursive: true, force: true }).catch(() => undefined);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
120
171
|
/** Hook the harness up to a session + audit log so task runs are durable. */
|
|
121
172
|
export async function runHarnessWithPersistence(tasks, opts) {
|
|
122
173
|
const store = new SessionStore(opts.storeDir);
|
|
@@ -134,8 +185,12 @@ export async function runHarnessWithPersistence(tasks, opts) {
|
|
|
134
185
|
type: r.details,
|
|
135
186
|
ts: Date.now(),
|
|
136
187
|
});
|
|
137
|
-
//
|
|
138
|
-
|
|
188
|
+
// Persist to session store
|
|
189
|
+
try {
|
|
190
|
+
const rec = await store.create({ cwd: process.cwd(), task: t.task, config: { model: 'mock', maxSteps: 12 } });
|
|
191
|
+
await store.setStatus(rec.id, r.status === 'pass' ? 'complete' : 'verify_failed', r.details);
|
|
192
|
+
}
|
|
193
|
+
catch { /* ignore */ }
|
|
139
194
|
}
|
|
140
195
|
const passed = results.filter((r) => r.status === 'pass').length;
|
|
141
196
|
return {
|
|
@@ -147,3 +202,19 @@ export async function runHarnessWithPersistence(tasks, opts) {
|
|
|
147
202
|
durationMs: Date.now() - start,
|
|
148
203
|
};
|
|
149
204
|
}
|
|
205
|
+
/** Compare two harness runs */
|
|
206
|
+
export function compareReports(a, b) {
|
|
207
|
+
const lines = [
|
|
208
|
+
`# Compare: ${a.passed}/${a.total} (${(a.passRate * 100).toFixed(1)}%) vs ${b.passed}/${b.total} (${(b.passRate * 100).toFixed(1)}%)`,
|
|
209
|
+
`Duration: ${(a.durationMs / 1000).toFixed(1)}s vs ${(b.durationMs / 1000).toFixed(1)}s`,
|
|
210
|
+
`| Task | A | B |`,
|
|
211
|
+
`|------|---|---|`,
|
|
212
|
+
];
|
|
213
|
+
const allIds = new Set([...a.results.map((r) => r.id), ...b.results.map((r) => r.id)]);
|
|
214
|
+
for (const id of allIds) {
|
|
215
|
+
const ra = a.results.find((r) => r.id === id);
|
|
216
|
+
const rb = b.results.find((r) => r.id === id);
|
|
217
|
+
lines.push(`| ${id} | ${ra?.status ?? '-'} | ${rb?.status ?? '-'} |`);
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -351,14 +351,39 @@ async function main() {
|
|
|
351
351
|
}
|
|
352
352
|
});
|
|
353
353
|
program
|
|
354
|
-
.command('eval
|
|
355
|
-
.description('Run
|
|
354
|
+
.command('eval [input]')
|
|
355
|
+
.description('Run eval harness: klyro eval --suite smoke | klyro eval <input.jsonl>')
|
|
356
356
|
.option('--output <mode>', 'Output mode: human (default), json (one JSON per line)')
|
|
357
|
+
.option('--suite <name>', 'Suite name (smoke, core, etc.) — loads from evals/fixtures')
|
|
358
|
+
.option('--filter <str>', 'Filter fixtures by name substring')
|
|
359
|
+
.option('--runs <n>', 'Runs per fixture (default 1)', (v) => parsePositiveInt('--runs', v))
|
|
360
|
+
.option('--parallel <n>', 'Parallelism (default 1)', (v) => parsePositiveInt('--parallel', v))
|
|
361
|
+
.option('--model <id>', 'Model for eval')
|
|
357
362
|
.action(async (input, opts) => {
|
|
358
363
|
const output = (opts.output ?? 'human');
|
|
359
|
-
|
|
364
|
+
if (opts.suite) {
|
|
365
|
+
const code = await runEval({ inputPath: input ?? '-', output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model });
|
|
366
|
+
process.exit(code);
|
|
367
|
+
}
|
|
368
|
+
if (!input) {
|
|
369
|
+
process.stderr.write('klyro eval: missing input (provide <input> or --suite)\n');
|
|
370
|
+
process.exit(2);
|
|
371
|
+
}
|
|
372
|
+
const code = await runEval({ inputPath: input, output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model });
|
|
360
373
|
process.exit(code);
|
|
361
374
|
});
|
|
375
|
+
program
|
|
376
|
+
.command('eval:compare <a> <b>')
|
|
377
|
+
.description('Compare two eval results JSON files')
|
|
378
|
+
.action(async (a, b) => {
|
|
379
|
+
const { compareReports } = await import('./eval/harness.js');
|
|
380
|
+
const fs = await import('node:fs/promises');
|
|
381
|
+
const ra = JSON.parse(await fs.readFile(a, 'utf-8'));
|
|
382
|
+
const rb = JSON.parse(await fs.readFile(b, 'utf-8'));
|
|
383
|
+
const out = compareReports(ra, rb);
|
|
384
|
+
process.stdout.write(out + '\n');
|
|
385
|
+
process.exit(0);
|
|
386
|
+
});
|
|
362
387
|
// Level 9 — Session management
|
|
363
388
|
const session = program.command('session').description('Session persistence (Level 9)');
|
|
364
389
|
session
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const askUserTool: import("../types.js").Tool<{
|
|
2
|
+
question: string;
|
|
3
|
+
options?: string[] | undefined;
|
|
4
|
+
}, {
|
|
5
|
+
readonly answer: string;
|
|
6
|
+
readonly question?: undefined;
|
|
7
|
+
readonly options?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
readonly question: string;
|
|
10
|
+
readonly options: string[] | undefined;
|
|
11
|
+
readonly answer?: undefined;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../types.js';
|
|
3
|
+
import { safe } from '../normalize.js';
|
|
4
|
+
const InputSchema = z.object({
|
|
5
|
+
question: z.string().min(1),
|
|
6
|
+
options: z.array(z.string()).optional(),
|
|
7
|
+
});
|
|
8
|
+
export const askUserTool = defineTool({
|
|
9
|
+
name: 'ask_user',
|
|
10
|
+
description: 'Ask the user a question (multiple choice or free text). Headless fails fast unless --auto-answer.',
|
|
11
|
+
inputSchema: InputSchema,
|
|
12
|
+
permission: 'read',
|
|
13
|
+
isConcurrencySafe: true,
|
|
14
|
+
execute: async (input, ctx) => {
|
|
15
|
+
return safe(async () => {
|
|
16
|
+
if (ctx.nonInteractive) {
|
|
17
|
+
// Check for auto-answer
|
|
18
|
+
const auto = process.env.KLYRO_AUTO_ANSWER;
|
|
19
|
+
if (auto)
|
|
20
|
+
return { answer: auto };
|
|
21
|
+
throw Object.assign(new Error(`ask_user requires interaction: ${input.question}`), { code: 'HEADLESS' });
|
|
22
|
+
}
|
|
23
|
+
// In TUI, this will be handled via ApprovalModal with ask_user kind
|
|
24
|
+
// For now, return as requires approval
|
|
25
|
+
return { question: input.question, options: input.options };
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../types.js';
|
|
3
|
+
import { safe } from '../normalize.js';
|
|
4
|
+
const TodoSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
title: z.string(),
|
|
7
|
+
status: z.enum(['pending', 'in_progress', 'done', 'failed', 'skipped']),
|
|
8
|
+
files: z.array(z.string()).optional(),
|
|
9
|
+
});
|
|
10
|
+
const InputSchema = z.object({
|
|
11
|
+
todos: z.array(TodoSchema),
|
|
12
|
+
});
|
|
13
|
+
export const todoWriteTool = defineTool({
|
|
14
|
+
name: 'todo_write',
|
|
15
|
+
description: 'Update the live plan checklist. Persisted and re-injected when stale.',
|
|
16
|
+
inputSchema: InputSchema,
|
|
17
|
+
permission: 'read',
|
|
18
|
+
isConcurrencySafe: true,
|
|
19
|
+
execute: async (input, ctx) => {
|
|
20
|
+
return safe(async () => {
|
|
21
|
+
// Persist to .klyro/plans/todos.json for 5.3
|
|
22
|
+
const fs = await import('node:fs/promises');
|
|
23
|
+
const path = await import('node:path');
|
|
24
|
+
const dir = path.join(ctx.cwd, '.klyro', 'plans');
|
|
25
|
+
await fs.mkdir(dir, { recursive: true });
|
|
26
|
+
const file = path.join(dir, 'todos.json');
|
|
27
|
+
await fs.writeFile(file, JSON.stringify(input.todos, null, 2), 'utf-8');
|
|
28
|
+
return { updated: input.todos.length };
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
package/dist/tools/registry.js
CHANGED
|
@@ -17,6 +17,8 @@ import { gitStatusTool } from './git/git-status.js';
|
|
|
17
17
|
import { gitDiffTool } from './git/git-diff.js';
|
|
18
18
|
import { gitLogTool } from './git/git-log.js';
|
|
19
19
|
import { runVerifyTool } from './verify/run-verify.js';
|
|
20
|
+
import { todoWriteTool } from './plan/todo-write.js';
|
|
21
|
+
import { askUserTool } from './plan/ask-user.js';
|
|
20
22
|
import { zodToJsonSchema } from './schema.js';
|
|
21
23
|
export class ToolRegistry {
|
|
22
24
|
tools = new Map();
|
|
@@ -89,5 +91,7 @@ export const builtinRegistry = () => {
|
|
|
89
91
|
r.register(gitDiffTool);
|
|
90
92
|
r.register(gitLogTool);
|
|
91
93
|
r.register(runVerifyTool);
|
|
94
|
+
r.register(todoWriteTool);
|
|
95
|
+
r.register(askUserTool);
|
|
92
96
|
return r;
|
|
93
97
|
};
|
package/dist/tui/app.js
CHANGED
|
@@ -31,6 +31,33 @@ export function App(props) {
|
|
|
31
31
|
});
|
|
32
32
|
const [elapsed, setElapsed] = useState(0);
|
|
33
33
|
const [queued, setQueued] = useState(null);
|
|
34
|
+
const [liveText, setLiveText] = useState('');
|
|
35
|
+
const batchRef = useRef('');
|
|
36
|
+
const batchTimer = useRef(null);
|
|
37
|
+
const flushBatch = useCallback(() => {
|
|
38
|
+
if (batchRef.current) {
|
|
39
|
+
const chunk = batchRef.current;
|
|
40
|
+
batchRef.current = '';
|
|
41
|
+
setLiveText((prev) => prev + chunk);
|
|
42
|
+
}
|
|
43
|
+
if (batchTimer.current) {
|
|
44
|
+
clearTimeout(batchTimer.current);
|
|
45
|
+
batchTimer.current = null;
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
const appendDeltaBatched = useCallback((text) => {
|
|
49
|
+
batchRef.current += text;
|
|
50
|
+
if (!batchTimer.current)
|
|
51
|
+
batchTimer.current = setTimeout(flushBatch, 33);
|
|
52
|
+
}, [flushBatch]);
|
|
53
|
+
const commitLive = useCallback(() => {
|
|
54
|
+
flushBatch();
|
|
55
|
+
if (liveText) {
|
|
56
|
+
const item = { id: nextId('text'), kind: 'text', text: liveText, role: 'assistant' };
|
|
57
|
+
setTranscript((prev) => [...prev, item]);
|
|
58
|
+
setLiveText('');
|
|
59
|
+
}
|
|
60
|
+
}, [liveText, flushBatch]);
|
|
34
61
|
useEffect(() => bridge.subscribe((p) => setAwaitingApproval(p !== null)), [bridge]);
|
|
35
62
|
// 2.4 — send queued when idle
|
|
36
63
|
useEffect(() => {
|
|
@@ -71,12 +98,16 @@ export function App(props) {
|
|
|
71
98
|
globalThis.__klyroAppAppend = append;
|
|
72
99
|
globalThis.__klyroAppStatus = updateStatus;
|
|
73
100
|
globalThis.__klyroAppPlan = updatePlan;
|
|
101
|
+
globalThis.__klyroAppendDelta = appendDeltaBatched;
|
|
102
|
+
globalThis.__klyroCommitLive = commitLive;
|
|
74
103
|
return () => {
|
|
75
104
|
delete globalThis.__klyroAppAppend;
|
|
76
105
|
delete globalThis.__klyroAppStatus;
|
|
77
106
|
delete globalThis.__klyroAppPlan;
|
|
107
|
+
delete globalThis.__klyroAppendDelta;
|
|
108
|
+
delete globalThis.__klyroCommitLive;
|
|
78
109
|
};
|
|
79
|
-
}, [append, updateStatus, updatePlan]);
|
|
110
|
+
}, [append, updateStatus, updatePlan, appendDeltaBatched, commitLive]);
|
|
80
111
|
// Single useInput owner — handles queued when running (2.4)
|
|
81
112
|
useInput((inputStr, key) => {
|
|
82
113
|
if (awaitingApproval)
|
|
@@ -129,5 +160,5 @@ export function App(props) {
|
|
|
129
160
|
const width = stdout?.columns ?? 100;
|
|
130
161
|
const height = stdout?.rows ?? 30;
|
|
131
162
|
const isSmall = width < 80;
|
|
132
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.
|
|
163
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height - 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: "KLYRO v0.1.14" }), _jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 API Usage Billing \u00B7 step ", status.step, "/", status.maxSteps, " \u00B7 ", status.status, " \u00B7 repairs ", status.repairs] }), _jsx(Text, { color: tokens.ansi.muted, children: props.cwd })] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingX: 1, paddingY: 1, children: [transcript.length === 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: tokens.ansi.muted, children: "No conversation yet. Try \"fix the failing login test\"" }) })) : (transcript.map((item) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: item.kind === 'text' && item.role === 'user' ? (_jsxs(Text, { children: ["\u203A ", item.text] })) : item.kind === 'text' ? (_jsxs(Text, { children: [" ", item.text] })) : item.kind === 'tool' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsxs(Text, { children: [item.name, " ", item.isError ? '✗' : '✓', " ", item.latencyMs ?? 0, "ms"] }), item.result ? _jsx(Text, { color: tokens.ansi.muted, children: String(item.result).slice(0, 200) }) : null] })) : item.kind === 'diff' ? (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: tokens.ansi.border, paddingX: 1, children: [_jsx(Text, { bold: true, children: item.summary ?? 'Diff' }), item.hunks.map((h, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: tokens.ansi.info, children: h.path }), h.lines.map((l, j) => (_jsxs(Text, { color: l.kind === 'add' ? tokens.ansi.success : l.kind === 'remove' ? tokens.ansi.error : tokens.ansi.muted, children: [l.kind === 'add' ? '+ ' : l.kind === 'remove' ? '- ' : ' ', l.text] }, j)))] }, i)))] })) : (_jsx(Transcript, { items: [item] })) }, item.id)))), liveText ? (_jsx(Box, { paddingLeft: 2, marginBottom: 1, children: _jsxs(Text, { children: [liveText, "\u258D"] }) })) : status.status === 'running' ? (_jsxs(Box, { children: [_jsx(Text, { color: tokens.ansi.info, children: "\u2726 Thinking..." }), _jsxs(Text, { color: tokens.ansi.muted, children: [" \u00B7 ", Math.round(elapsed / 1000), "s"] })] })) : null, plan.length > 0 ? _jsx(PlanView, { steps: plan, expanded: false, onToggle: () => { } }) : null] }), _jsxs(Box, { borderStyle: "single", borderColor: tokens.ansi.accent, paddingX: 1, children: [_jsx(Text, { children: "\u203A " }), _jsxs(Text, { children: [input, "\u258F"] })] }), _jsxs(Box, { justifyContent: "space-between", paddingX: 1, borderStyle: "single", borderColor: tokens.ansi.border, children: [_jsxs(Text, { color: tokens.ansi.muted, children: [status.model, " \u00B7 ", status.usageInput + status.usageOutput, " tokens \u00B7 $", (status.usageInput / 1000 * 0.003 + status.usageOutput / 1000 * 0.015).toFixed(2), " \u00B7 ", Math.round(elapsed / 1000), "s"] }), _jsx(Text, { color: tokens.ansi.muted, children: isSmall ? 'Ctrl+C interrupt' : 'Ctrl+C interrupt · Ctrl+O expand · ↑↓ scroll' })] })] }));
|
|
133
164
|
}
|