jaitus 0.3.4 → 0.3.6
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 +35 -37
- package/bin/jaitus.js +8 -0
- package/package.json +2 -2
- package/src/agent/loop.js +186 -28
- package/src/agent/permissions.js +50 -17
- package/src/agent/rules.js +85 -14
- package/src/agent/tools.js +266 -50
- package/src/background.js +34 -7
- package/src/commands/commit.js +63 -46
- package/src/commands/doctor.js +1 -1
- package/src/commands/explain.js +5 -5
- package/src/commands/logout.js +2 -0
- package/src/commands/research.js +5 -6
- package/src/commands/review.js +8 -11
- package/src/commands/test.js +18 -14
- package/src/config.js +12 -1
- package/src/errors.js +4 -0
- package/src/index.js +567 -118
- package/src/mcp.js +209 -72
- package/src/project.js +0 -0
- package/src/session.js +18 -0
- package/src/sessions.js +20 -2
- package/src/terminal.js +46 -11
- package/src/tui/ansi.js +3 -1
- package/src/tui/clipboard.js +100 -7
- package/src/tui/entries.js +5 -5
- package/src/tui/highlight.js +4 -4
- package/src/tui/markdown.js +1 -1
- package/src/tui/theme.js +8 -0
- package/src/tui-ink.js +510 -106
- package/src/tui.js +4 -0
- package/src/ui.js +5 -1
package/src/background.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// with background_read; /tasks lists jobs with attach/stop. `! cmd &` and
|
|
6
6
|
// Ctrl+B start one from the prompt; background_run does it from the model.
|
|
7
7
|
|
|
8
|
-
import { spawn } from 'node:child_process';
|
|
8
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
@@ -36,6 +36,7 @@ export function startBackground(command, cwd) {
|
|
|
36
36
|
child.on('close', (code) => {
|
|
37
37
|
job.exit = code ?? 'signal';
|
|
38
38
|
try { fs.appendFileSync(log, `\n[exit ${job.exit}]\n`); } catch { /* log gone */ }
|
|
39
|
+
try { fs.closeSync(out); } catch { /* already closed */ }
|
|
39
40
|
});
|
|
40
41
|
try { child.unref(); } catch { /* stay attached */ }
|
|
41
42
|
job.child = child;
|
|
@@ -61,9 +62,19 @@ function findJob(id) {
|
|
|
61
62
|
export function readBackground(id, tail = 40) {
|
|
62
63
|
const job = findJob(id);
|
|
63
64
|
if (!job) return null;
|
|
65
|
+
// Only the end of the log: a dev server's log grows for hours, and reading all of it on
|
|
66
|
+
// every poll put the whole file in memory each time.
|
|
64
67
|
let lines = [];
|
|
65
68
|
try {
|
|
66
|
-
|
|
69
|
+
const size = fs.statSync(job.log).size;
|
|
70
|
+
const from = Math.max(0, size - 64 * 1024);
|
|
71
|
+
const fd = fs.openSync(job.log, 'r');
|
|
72
|
+
try {
|
|
73
|
+
const buf = Buffer.alloc(size - from);
|
|
74
|
+
fs.readSync(fd, buf, 0, buf.length, from);
|
|
75
|
+
lines = buf.toString('utf8').split('\n');
|
|
76
|
+
if (from > 0) lines.shift();
|
|
77
|
+
} finally { fs.closeSync(fd); }
|
|
67
78
|
} catch { /* empty so far */ }
|
|
68
79
|
return {
|
|
69
80
|
id: job.id, command: job.command, running: job.exit === null, exit: job.exit,
|
|
@@ -75,11 +86,27 @@ export function stopBackground(id) {
|
|
|
75
86
|
const job = findJob(id);
|
|
76
87
|
if (!job) return false;
|
|
77
88
|
if (job.exit !== null) return true;
|
|
89
|
+
killJob(job);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The whole tree. On Windows child.kill() ended only cmd.exe: the dev server it started
|
|
94
|
+
// kept running and holding its port while the tool said "Stopped".
|
|
95
|
+
function killJob(job) {
|
|
96
|
+
const pid = job.child?.pid;
|
|
97
|
+
if (!pid) return;
|
|
78
98
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try { process.kill(-job.child.pid); } catch { /* already gone */ }
|
|
82
|
-
}
|
|
99
|
+
if (process.platform === 'win32') spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
100
|
+
else { try { process.kill(-pid, 'SIGTERM'); } catch { job.child.kill('SIGTERM'); } }
|
|
83
101
|
} catch { /* already gone */ }
|
|
84
|
-
return true;
|
|
85
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stop every job still running. Called when Jaitus exits: jobs used to outlive it, and
|
|
106
|
+
* after a restart nothing could see or stop them — a dev server holding a port with no
|
|
107
|
+
* way back to it.
|
|
108
|
+
*/
|
|
109
|
+
export function stopAllBackground() {
|
|
110
|
+
for (const job of jobs.values()) if (job.exit === null) killJob(job);
|
|
111
|
+
}
|
|
112
|
+
process.once('exit', stopAllBackground);
|
package/src/commands/commit.js
CHANGED
|
@@ -1,71 +1,88 @@
|
|
|
1
1
|
// ─── /commit — review the diff, write a real message, land it ───────────────────
|
|
2
2
|
//
|
|
3
|
-
// Shows what changed, asks the model to summarise the WHY into a subject line,
|
|
4
|
-
//
|
|
3
|
+
// Shows what changed, asks the model to summarise the WHY into a subject line, then
|
|
4
|
+
// asks before committing. Pushing is always a separate, explicit choice — never implicit.
|
|
5
|
+
//
|
|
6
|
+
// /commit the model proposes a subject; you confirm
|
|
7
|
+
// /commit fix the login bug your subject, used as written; you still confirm
|
|
8
|
+
//
|
|
9
|
+
// The previous version could never commit through its normal path: it did not await
|
|
10
|
+
// its own prompts (so the answer was a Promise, never 'y'), read the subject from a
|
|
11
|
+
// field the agent does not return, and ran git in the process directory rather than the
|
|
12
|
+
// session's --cwd.
|
|
5
13
|
import { spawnSync } from 'node:child_process';
|
|
6
14
|
import { runAgent } from '../agent/loop.js';
|
|
7
15
|
import { createPermissions } from '../agent/permissions.js';
|
|
8
16
|
import { c, out, ok, warn, fail, askKey, glyph } from '../ui.js';
|
|
9
17
|
|
|
10
|
-
function git(args,
|
|
11
|
-
const r = spawnSync('git', args, { encoding: 'utf8',
|
|
18
|
+
function git(args, cwd) {
|
|
19
|
+
const r = spawnSync('git', args, { encoding: 'utf8', cwd, windowsHide: true });
|
|
12
20
|
return { ok: r.status === 0, out: (r.stdout || '').trim(), err: (r.stderr || '').trim() };
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
/** A choice, through the app's dialog when there is one, else a key prompt. */
|
|
24
|
+
async function choose(ctx, question, options) {
|
|
25
|
+
if (ctx.tui?.askDialog) {
|
|
26
|
+
const r = await ctx.tui.askDialog(question, options.map(([value, label]) => ({ value, label })), [], { title: 'COMMIT', hideComment: true });
|
|
27
|
+
return r && typeof r === 'object' ? r.value : r;
|
|
28
|
+
}
|
|
29
|
+
if (!process.stdin.isTTY) return null;
|
|
30
|
+
const keys = options.map(([value]) => value[0]);
|
|
31
|
+
const key = await askKey(` ${question} ${options.map(([v, l]) => `(${v[0]}) ${l}`).join(' / ')}`, keys);
|
|
32
|
+
return options.find(([value]) => value[0] === key)?.[0] ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
export default async function commit(args, ctx) {
|
|
16
|
-
|
|
36
|
+
const cwd = ctx.flags?.cwd || process.cwd();
|
|
37
|
+
if (!git(['rev-parse', '--is-inside-work-tree'], cwd).ok) {
|
|
17
38
|
fail('Not a git repository.');
|
|
18
39
|
return null;
|
|
19
40
|
}
|
|
20
|
-
const status = git(['status', '--porcelain']);
|
|
41
|
+
const status = git(['status', '--porcelain'], cwd);
|
|
21
42
|
if (!status.out) { ok('Nothing to commit — the working tree is clean.'); return null; }
|
|
22
43
|
|
|
23
|
-
// What the commit would contain, shown before anything happens
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (n > 0) out(c.dim(` ${glyph('…', '...')} and ${n} more files`));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// The model writes the message from the actual diff — with a firm brevity
|
|
33
|
-
// leash, because left alone every model writes essays.
|
|
34
|
-
let subject = '';
|
|
35
|
-
try {
|
|
36
|
-
const diff = git(['diff', 'HEAD']).out.slice(0, 12000);
|
|
37
|
-
const side = await runAgent({
|
|
38
|
-
client: ctx.client, cwd: ctx.flags.cwd,
|
|
39
|
-
permissions: createPermissions({ mode: 'plan' }),
|
|
40
|
-
messages: [{ role: 'user', content: `Write a git commit subject for this diff. Rules: imperative mood, no prefix, max 72 chars, say WHAT changed and WHY. Answer with the subject line ONLY — no quotes, no body, no markdown.\n\n${diff}` }],
|
|
41
|
-
});
|
|
42
|
-
const text = (side?.messages || []).filter((m) => m.role === 'assistant').pop();
|
|
43
|
-
const content = Array.isArray(text?.content) ? text.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ') : (text?.content || '');
|
|
44
|
-
subject = content.trim().split('\n')[0].replace(/^["'`]|["'`]$/g, '').slice(0, 72);
|
|
45
|
-
} catch { /* fall through to the manual path */ }
|
|
44
|
+
// What the commit would contain, shown before anything happens — including files git
|
|
45
|
+
// does not track yet, which `git add -A` below will add.
|
|
46
|
+
const lines = status.out.split('\n');
|
|
47
|
+
out(c.dim(` ${lines.length} file${lines.length === 1 ? '' : 's'} changed:`));
|
|
48
|
+
for (const line of lines.slice(0, 20)) out(` ${c.dim(line)}`);
|
|
49
|
+
if (lines.length > 20) out(c.dim(` ${glyph('…', '...')} and ${lines.length - 20} more`));
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
let subject = args.join(' ').trim();
|
|
52
|
+
if (!subject) {
|
|
53
|
+
// The model writes the message from the actual diff — with a firm brevity leash,
|
|
54
|
+
// because left alone every model writes essays.
|
|
55
|
+
const diff = git(['diff', 'HEAD'], cwd).out.slice(0, 12000) || status.out;
|
|
56
|
+
let text = '';
|
|
57
|
+
const messages = [{ role: 'user', content: `Write a git commit subject for this change. Rules: imperative mood, no prefix, max 72 chars, say WHAT changed and WHY. Answer with the subject line ONLY — no quotes, no body, no markdown.\n\n${diff}` }];
|
|
58
|
+
try {
|
|
59
|
+
if (ctx.runSide) await ctx.runSide({ messages, mode: 'plan', onText: (t) => { text += t; } });
|
|
60
|
+
else await runAgent({ client: ctx.client, model: ctx.model, cwd, permissions: createPermissions({ mode: 'plan' }), messages, silent: true, onText: (t) => { text += t; } });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
warn(`Could not write a subject (${e.message}). Give one yourself: /commit <subject>`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
subject = text.trim().split('\n')[0].replace(/^["'`]|["'`]$/g, '').slice(0, 72);
|
|
66
|
+
if (!subject) { warn('No subject came back. Give one yourself: /commit <subject>'); return null; }
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
out(` ${c.dim('Subject:')} ${c.bold(subject)}`);
|
|
70
|
+
const answer = await choose(ctx, `Commit ${lines.length} file${lines.length === 1 ? '' : 's'} as "${subject}"?`, [
|
|
71
|
+
['commit', 'Commit'],
|
|
72
|
+
['push', 'Commit and push'],
|
|
73
|
+
['no', 'Cancel'],
|
|
74
|
+
]);
|
|
75
|
+
if (answer !== 'commit' && answer !== 'push') { warn('Commit cancelled. To use your own subject: /commit <subject>'); return null; }
|
|
76
|
+
|
|
77
|
+
const add = git(['add', '-A'], cwd);
|
|
60
78
|
if (!add.ok) { fail(`Could not stage: ${add.err}`); return null; }
|
|
61
|
-
const done = git(['commit', '-m',
|
|
79
|
+
const done = git(['commit', '-m', subject], cwd);
|
|
62
80
|
if (!done.ok) { fail(`Commit failed: ${done.err || done.out}`); return null; }
|
|
63
|
-
ok(`Committed: ${
|
|
81
|
+
ok(`Committed: ${subject}`);
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const pushed = git(['push', 'origin', branch]);
|
|
83
|
+
if (answer === 'push') {
|
|
84
|
+
const branch = git(['branch', '--show-current'], cwd).out || 'main';
|
|
85
|
+
const pushed = git(['push', 'origin', branch], cwd);
|
|
69
86
|
if (pushed.ok) ok(`Pushed to origin/${branch}.`);
|
|
70
87
|
else fail(`Push failed: ${pushed.err || pushed.out}`);
|
|
71
88
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -108,7 +108,7 @@ export default async function doctor(argv = [], { json = false } = {}) {
|
|
|
108
108
|
// ── The account ───────────────────────────────────────────────────────────────
|
|
109
109
|
if (key && reachable) {
|
|
110
110
|
try {
|
|
111
|
-
const res = await reach(`${baseUrl}/api/auth/me`, { headers: {
|
|
111
|
+
const res = await reach(`${baseUrl}/api/auth/me`, { headers: { authorization: `Bearer ${key}` } });
|
|
112
112
|
const body = await res.json().catch(() => ({}));
|
|
113
113
|
const u = body.user || body.data || {};
|
|
114
114
|
if (res.ok && (u.email || u.id)) {
|
package/src/commands/explain.js
CHANGED
|
@@ -24,10 +24,10 @@ export default async function explain(args, ctx) {
|
|
|
24
24
|
: `Explain the concept "${topic}" grounded in this project where it applies — read files to find real usages rather than answering abstractly. Keep it under 250 words.`;
|
|
25
25
|
|
|
26
26
|
const sideMessages = [{ role: 'user', content: brief }];
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const extraSystem = `You are the Jaitus explainer. Read-only: never edit files or run commands. Show file:line references for every claim about the code. Prefer short paragraphs over walls of text.`;
|
|
28
|
+
// In the app: the transcript, Esc stops it, the session's own rules (see runSide).
|
|
29
|
+
const side = ctx.runSide
|
|
30
|
+
? await ctx.runSide({ messages: sideMessages, mode: 'plan', extraSystem })
|
|
31
|
+
: await runAgent({ client: ctx.client, messages: sideMessages, model: ctx.model, cwd: ctx.flags.cwd, permissions: createPermissions({ mode: 'plan' }), extraSystem });
|
|
32
32
|
return null;
|
|
33
33
|
}
|
package/src/commands/logout.js
CHANGED
|
@@ -12,6 +12,8 @@ import { ok, info, warn } from '../ui.js';
|
|
|
12
12
|
export default async function logout() {
|
|
13
13
|
const { hadSession, server } = await signOut();
|
|
14
14
|
if (!hadSession) {
|
|
15
|
+
// JAITUS_TOKEN signs every request by itself, so "not signed in" was untrue with it.
|
|
16
|
+
if (process.env.JAITUS_TOKEN) { info('This terminal signs in with JAITUS_TOKEN. Unset that variable to sign out; there is no saved sign-in to remove.'); return 0; }
|
|
15
17
|
info('You were not signed in.');
|
|
16
18
|
return 0;
|
|
17
19
|
}
|
package/src/commands/research.js
CHANGED
|
@@ -48,12 +48,11 @@ export default async function research(args, ctx) {
|
|
|
48
48
|
].join('\n');
|
|
49
49
|
|
|
50
50
|
const sideMessages = [{ role: 'user', content: brief }];
|
|
51
|
-
const sidePermissions = createPermissions({ mode: 'plan' }); // read-only: web tools only
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
51
|
+
const sidePermissions = createPermissions({ mode: 'plan', allow: ['web_fetch'] }); // read-only: web tools only
|
|
52
|
+
const extraSystem = `You are the Jaitus research agent. You find things out on the web and report back with sources. You never edit files or run commands — web_search and web_fetch only. Be precise about what a source actually says; never invent a URL.`;
|
|
53
|
+
const side = ctx.runSide
|
|
54
|
+
? await ctx.runSide({ messages: sideMessages, mode: 'plan', extraSystem, allow: ['web_fetch'] })
|
|
55
|
+
: await runAgent({ client: ctx.client, messages: sideMessages, model: ctx.model, cwd: ctx.flags.cwd, permissions: sidePermissions, extraSystem });
|
|
57
56
|
|
|
58
57
|
ok(`Research complete (${side.tools || 0} tool calls, ${((side.input || 0) + (side.output || 0)).toLocaleString()} tokens).`);
|
|
59
58
|
return null;
|
package/src/commands/review.js
CHANGED
|
@@ -64,17 +64,14 @@ export async function runReview(mode, args, ctx) {
|
|
|
64
64
|
role: 'user',
|
|
65
65
|
content: `${brief.task(target)}\n\nDiff${target ? ` (${target})` : ''}:\n${diff.out.slice(0, 30000)}${diff.out.length > 30000 ? `\n${glyph('…', '...')}(truncated)` : ''}`,
|
|
66
66
|
}];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
? `${brief.system}\n\nAfter reporting, APPLY the fixes you recommended (minimal diffs, same style as the file). Then summarize what changed.`
|
|
76
|
-
: brief.system,
|
|
77
|
-
});
|
|
67
|
+
// Reviewing reads; --fix writes — so the fix pass inherits the session's own
|
|
68
|
+
// permission mode AND its allow/deny rules instead of smuggling in fresh ones.
|
|
69
|
+
const runMode = fix ? (ctx.permissions?.mode || 'default') : 'plan';
|
|
70
|
+
const extraSystem = fix
|
|
71
|
+
? `${brief.system}\n\nAfter reporting, APPLY the fixes you recommended (minimal diffs, same style as the file). Then summarize what changed.`
|
|
72
|
+
: brief.system;
|
|
73
|
+
if (ctx.runSide) await ctx.runSide({ messages, mode: runMode, extraSystem });
|
|
74
|
+
else await runAgent({ client: ctx.client, messages, model: ctx.model, cwd: ctx.flags.cwd, permissions: createPermissions({ mode: runMode }), extraSystem });
|
|
78
75
|
if (!fix) warn(`Run with --fix to let the agent apply the findings (permissions still ask first).`);
|
|
79
76
|
return null;
|
|
80
77
|
}
|
package/src/commands/test.js
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// only after permission, and re-runs until green or you stop it.
|
|
7
7
|
//
|
|
8
8
|
// Usage: /test [anything forwarded to the runner]
|
|
9
|
-
import { spawnSync } from 'node:child_process';
|
|
10
9
|
import fs from 'node:fs';
|
|
11
10
|
import path from 'node:path';
|
|
12
11
|
import { ok, warn, fail, out, c } from '../ui.js';
|
|
@@ -33,18 +32,25 @@ export default async function test(args, ctx) {
|
|
|
33
32
|
const runner = detectRunner(ctx.flags.cwd);
|
|
34
33
|
if (!runner) { warn('No test runner found (package.json scripts, pytest, go, cargo).'); return null; }
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const command = [runner.cmd, ...runner.args, ...forward].join(' ');
|
|
36
|
+
out(c.dim(` Running: ${command} (Esc stops it)`));
|
|
37
|
+
// Through the same runner as the agent's commands: a shell (on Windows `npm` is
|
|
38
|
+
// npm.cmd, and spawnSync without one failed every time — "Tests failed (exit null)"),
|
|
39
|
+
// the whole process tree killed on Esc or at the limit, output capped — and it no
|
|
40
|
+
// longer freezes the interface while the suite runs.
|
|
41
|
+
const { byName } = await import('../agent/tools.js');
|
|
42
|
+
const run = (signal) => byName.run_command.run({ command, timeout_ms: 600_000 }, { cwd: ctx.flags.cwd, signal });
|
|
43
|
+
const output = String(ctx.abortable ? await ctx.abortable(run) : await run(null));
|
|
44
|
+
const status = Number((/exit code: (\d+)/.exec(output) || [])[1]);
|
|
45
|
+
const body = output.replace(/\n*exit code: .*$/s, '');
|
|
41
46
|
|
|
42
47
|
// Show the tail — enough to see failures, not the whole log.
|
|
43
|
-
const lines =
|
|
48
|
+
const lines = body.split('\n');
|
|
44
49
|
for (const line of lines.slice(-40)) out(` ${line}`);
|
|
45
50
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
51
|
+
if (/\[stopped by the user\]/.test(output)) { warn('Test run stopped.'); return null; }
|
|
52
|
+
if (status === 0) { ok('All tests passed.'); return null; }
|
|
53
|
+
fail(`Tests failed (exit ${Number.isFinite(status) ? status : 'none'}).`);
|
|
48
54
|
|
|
49
55
|
if (!wantsFix) {
|
|
50
56
|
warn('Run /test --fix to let the agent fix the failures.');
|
|
@@ -59,10 +65,8 @@ export default async function test(args, ctx) {
|
|
|
59
65
|
role: 'user',
|
|
60
66
|
content: `These tests are failing in ${ctx.flags.cwd}. Diagnose and FIX them: read the failing code, make the minimal correct changes, then run \`${runner.label}\` again. Do not weaken assertions to make tests pass unless the test itself is provably wrong.\n\nFailure output (tail):\n${output.slice(-12000)}`,
|
|
61
67
|
}];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
extraSystem: `You are fixing failing tests. Minimal, correct changes. Never delete a failing test to go green; fix the code (or fix the test only if the test is wrong).`,
|
|
66
|
-
});
|
|
68
|
+
const extraSystem = `You are fixing failing tests. Minimal, correct changes. Never delete a failing test to go green; fix the code (or fix the test only if the test is wrong).`;
|
|
69
|
+
if (ctx.runSide) await ctx.runSide({ messages: fixMessages, mode: ctx.permissions?.mode || 'default', extraSystem });
|
|
70
|
+
else await runAgent({ client: ctx.client, messages: fixMessages, model: ctx.model, cwd: ctx.flags.cwd, permissions: createPermissions({ mode: ctx.permissions?.mode || 'default' }), extraSystem });
|
|
67
71
|
return null;
|
|
68
72
|
}
|
package/src/config.js
CHANGED
|
@@ -75,8 +75,19 @@ export function loadSettings() {
|
|
|
75
75
|
return readJson(SETTINGS, {});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// Whoever is showing settings live (the app view) is told when they change. /scroll-speed,
|
|
79
|
+
// /animations, /config mouse and copyOnSelect used to say "saved" and do nothing until a
|
|
80
|
+
// restart, because the app had read them once at start.
|
|
81
|
+
const settingsListeners = new Set();
|
|
82
|
+
export function onSettingsChange(fn) {
|
|
83
|
+
settingsListeners.add(fn);
|
|
84
|
+
return () => settingsListeners.delete(fn);
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
export function saveSettings(settings) {
|
|
79
|
-
|
|
88
|
+
// 0600: it can hold search API keys (braveSearchKey, tavilyKey, serperKey).
|
|
89
|
+
writeJson(SETTINGS, settings, 0o600);
|
|
90
|
+
for (const fn of settingsListeners) { try { fn(settings); } catch { /* a listener never breaks a save */ } }
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
package/src/errors.js
CHANGED
|
@@ -17,6 +17,10 @@ const CATALOG = {
|
|
|
17
17
|
plan_limit_reached: { title: 'Monthly allowance used', hint: 'It resets at the start of your next period, or upgrade on the website. /usage shows where you are.' },
|
|
18
18
|
rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
|
|
19
19
|
context_too_long: { title: 'Conversation too long', hint: 'Run /compact, or start a new session.' },
|
|
20
|
+
request_too_large: { title: 'Request too large', hint: 'Usually images: remove some, or run /compact to drop old ones. Images over 3.5 MB are shrunk automatically.' },
|
|
21
|
+
daily_limit_reached: { title: 'Daily limit reached', hint: 'Your plan\'s daily allowance resets tomorrow. /usage shows where you are.' },
|
|
22
|
+
token_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
23
|
+
refresh_race: { title: 'Sign-in was being renewed', hint: 'Try again.' },
|
|
20
24
|
content_blocked: { title: 'Request refused', hint: 'Rephrase the request and try again.' },
|
|
21
25
|
bad_request: { title: 'Request not accepted', hint: 'Try again; if it repeats, run /debug and contact support.' },
|
|
22
26
|
model_unavailable: { title: 'Model unavailable', hint: 'Run /model to pick another.' },
|