ikarmic-forge 4.4.0 → 4.5.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 +4 -0
- package/dist/acp/server.d.ts +3 -1
- package/dist/acp/server.d.ts.map +1 -1
- package/dist/acp/server.js +45 -4
- package/dist/acp/server.js.map +1 -1
- package/dist/bin/forge.js +45 -6
- package/dist/bin/forge.js.map +1 -1
- package/dist/chat-files.d.ts +8 -0
- package/dist/chat-files.d.ts.map +1 -1
- package/dist/chat-files.js +41 -4
- package/dist/chat-files.js.map +1 -1
- package/dist/clipboard.d.ts +38 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +63 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands/memory.d.ts +10 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +77 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/spec.d.ts +13 -3
- package/dist/commands/spec.d.ts.map +1 -1
- package/dist/commands/spec.js +76 -5
- package/dist/commands/spec.js.map +1 -1
- package/dist/components/Approval.d.ts.map +1 -1
- package/dist/components/Approval.js +2 -0
- package/dist/components/Approval.js.map +1 -1
- package/dist/options.d.ts +45 -3
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +90 -11
- package/dist/options.js.map +1 -1
- package/dist/repl-commands.d.ts +39 -0
- package/dist/repl-commands.d.ts.map +1 -1
- package/dist/repl-commands.js +91 -4
- package/dist/repl-commands.js.map +1 -1
- package/dist/repl-extras.d.ts +39 -0
- package/dist/repl-extras.d.ts.map +1 -0
- package/dist/repl-extras.js +120 -0
- package/dist/repl-extras.js.map +1 -0
- package/dist/repl.d.ts +25 -1
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +419 -86
- package/dist/repl.js.map +1 -1
- package/dist/session-checkpoints.js +1 -1
- package/dist/session-checkpoints.js.map +1 -1
- package/package.json +2 -2
package/dist/repl.js
CHANGED
|
@@ -1,19 +1,49 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Box, Text, useInput } from 'ink';
|
|
2
|
+
import { Box, Text, useInput, useStdin } from 'ink';
|
|
3
3
|
import TextInput from 'ink-text-input';
|
|
4
|
-
import { Agent, formatSteeringLine, defaultSessionDir, saveSession, SPEC_READONLY_TOOLS, SpecStore, splitSpecSections, slugifyTaskForSpecId, } from '@ikarmic/forge-core';
|
|
5
|
-
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { Agent, formatSteeringLine, defaultSessionDir, expandCustomCommand, formatCustomCommands, listSessions, loadCustomCommands, saveSession, searchSessions, SPEC_READONLY_TOOLS, SpecStore, splitSpecSections, slugifyTaskForSpecId, } from '@ikarmic/forge-core';
|
|
5
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { buildRepoMap, changedFiles, createExploreEditRouter, lintFile, commitFiles, diffForFiles, generateCommitMessage, isGitRepo, undoLastForgeCommit, } from '@ikarmic/forge-core';
|
|
7
|
+
import { buildRepoMap, changedFiles, createExploreEditRouter, fetchUrlTool, lintFile, redact, commitFiles, diffForFiles, generateCommitMessage, isGitRepo, undoLastForgeCommit, } from '@ikarmic/forge-core';
|
|
8
8
|
import { Approval } from './components/Approval.js';
|
|
9
|
-
import { ChatFiles,
|
|
9
|
+
import { ChatFiles, stripAttachedFiles } from './chat-files.js';
|
|
10
|
+
import { copyToClipboard, readClipboard } from './clipboard.js';
|
|
11
|
+
import { buildProgram } from './options.js';
|
|
12
|
+
import { composeInEditor, conversationMarkdown, helpQuestionTask, lastAssistantText, reportUrl, } from './repl-extras.js';
|
|
10
13
|
import { formatCommitLine, gitCommitOptions } from './git-workflow.js';
|
|
11
|
-
import { OK_TASK, describeModel, estimateContextTokens, fixTask, formatModels, formatTokens, parseSlashCommand, runContextNote, runShell, switchModel, } from './repl-commands.js';
|
|
14
|
+
import { OK_TASK, describeModel, formatScript, parseReasoningEffort, parseThinkTokens, scriptLines, estimateContextTokens, fixTask, formatModels, formatTokens, parseSlashCommand, runContextNote, runShell, switchModel, } from './repl-commands.js';
|
|
12
15
|
import { Header } from './components/Header.js';
|
|
13
16
|
import { Spinner } from './components/Spinner.js';
|
|
14
17
|
import { sessionCheckpointStore, sessionCheckpointsConfig } from './session-checkpoints.js';
|
|
15
18
|
import { getUnifiedDiffText, summarizeResult as summarizeToolResult } from './tool-display.js';
|
|
16
|
-
|
|
19
|
+
/** The text of `/help`. */
|
|
20
|
+
function replHelpText(sessionId) {
|
|
21
|
+
return [
|
|
22
|
+
'Slash commands: /help [question] /clear /reset /save [label] /plan <task> /spec <task> /compact /rewind /ok /model /weak-model /models /add /read-only /drop /ls /run /test /lint /tokens /settings /commit /undo /diff /map /map-refresh /copy /copy-context /paste /web /editor /report /reasoning-effort /think-tokens /editor-model /architect /load /save-script /retry /edit-last /sessions /exit /quit.',
|
|
23
|
+
'/retry [new text] runs your last message again (or an edited one) in place of the last turn; /edit-last opens it in your editor first. Files the dropped turn changed stay changed: /undo or /rewind reverts them.',
|
|
24
|
+
'/sessions [words] lists recent sessions, or searches all of them; resume one with forge --resume <id>. Messages typed while a turn runs wait in a queue and run in order.',
|
|
25
|
+
'Custom commands: a Markdown file in .forge/commands/<name>.md (project) or ~/.forge/commands/<name>.md becomes /<name> [args]; $ARGUMENTS and $1..$9 in it are filled in.',
|
|
26
|
+
'/commit commits all pending changes with a drafted message; /undo reverts the last Forge commit; /diff shows uncommitted changes.',
|
|
27
|
+
'/model [id|alias|provider:model] switches (or shows) the model; /weak-model sets the cheap model; /models [query] lists aliases and known models.',
|
|
28
|
+
'/add <files> and /read-only <files> attach files to every turn (images and PDFs go to the model as they are); /drop [files] detaches; /ls lists them. /run <cmd> (or !<cmd>) runs a command and shows the model its output on the next turn; /test [cmd] and /lint [files] send failures to the model to fix; with no argument they run your --test-cmd / --lint-cmd commands.',
|
|
29
|
+
'/ok says "go ahead"; /reset detaches files and clears the conversation; /tokens shows context use; /settings shows the effective settings.',
|
|
30
|
+
'/map shows the repo map (--map-tokens sets the size sent to the model); /map-refresh re-parses every file first.',
|
|
31
|
+
'/copy copies the last reply to the clipboard; /copy-context copies the whole conversation as Markdown; /paste adds the clipboard text to your next message; /web <url> adds a web page to it.',
|
|
32
|
+
'/editor [text] (or /edit) writes your message in $VISUAL / $EDITOR and sends it; /report prints a prefilled GitHub issue link with diagnostics only; /help <question> answers a question about Forge.',
|
|
33
|
+
'/reasoning-effort <level|default> sets how hard the model thinks; /think-tokens <n|8k|0> picks the nearest level for a thinking budget (Forge sets a level, not an exact count).',
|
|
34
|
+
'/editor-model <model> and /architect <task>: the main model plans without tools (like /plan), you approve, then the editor model applies it. Attach files (/add) and use /map to give the planner context.',
|
|
35
|
+
'/load <file> runs the commands and messages in a file, one per line (# for comments); /save-script <file> writes the model and attached files as such a script.',
|
|
36
|
+
'Otherwise type a task and press Enter.',
|
|
37
|
+
`Session: ${sessionId} (auto-saved after each turn).`,
|
|
38
|
+
'Plan review: when a plan is proposed, type a (approve), e (edit), or c (cancel).',
|
|
39
|
+
'Spec review: same a/e/c keys: approve writes requirements.md/design.md/tasks.md to .forge/specs/<id>/.',
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
|
42
|
+
async function defaultFetchPage(url, cwd) {
|
|
43
|
+
const r = await fetchUrlTool.execute({ url }, { cwd, auto: false });
|
|
44
|
+
return { url: r.url, ...(r.title ? { title: r.title } : {}), status: r.status, excerpt: r.excerpt, truncated: r.truncated };
|
|
45
|
+
}
|
|
46
|
+
export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initialHistory, initialLabel, sessionDir, display = {}, gitWorkflow = null, gitOptions = { noAttribute: false, gitCommitVerify: false }, commitModel, notify, aliases, version = 'unknown', services, lintCmds = [], testCmds = [], }) {
|
|
17
47
|
const [messages, setMessages] = useState([]);
|
|
18
48
|
const [input, setInput] = useState('');
|
|
19
49
|
const [running, setRunning] = useState(false);
|
|
@@ -49,6 +79,34 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
49
79
|
const pendingNotesRef = useRef([]);
|
|
50
80
|
/** `/weak-model`: cheap model for read-only exploration steps and commit messages. */
|
|
51
81
|
const [weakModel, setWeakModel] = useState(undefined);
|
|
82
|
+
/** `/editor-model`: the model that applies a plan made with `/architect`. */
|
|
83
|
+
const [editorModel, setEditorModel] = useState(undefined);
|
|
84
|
+
/** Set while an `/architect` plan awaits approval: the model to apply it with. */
|
|
85
|
+
const architectEditorRef = useRef(null);
|
|
86
|
+
/** True while a `/load` script runs, so a script cannot load another. */
|
|
87
|
+
const loadingRef = useRef(false);
|
|
88
|
+
/** The latest `handleSubmit`, for `/load` to replay lines through. */
|
|
89
|
+
const submitRef = useRef(null);
|
|
90
|
+
/** The last chat message and the history before it, for `/retry` and `/edit-last`. */
|
|
91
|
+
const lastTurnRef = useRef(null);
|
|
92
|
+
/** Messages typed while a submission is being handled; they run in order afterwards. */
|
|
93
|
+
const queueRef = useRef([]);
|
|
94
|
+
const inFlightRef = useRef(false);
|
|
95
|
+
const { setRawMode, isRawModeSupported } = useStdin();
|
|
96
|
+
/** Hand the terminal to the editor: leave raw mode while it runs, then take it back. */
|
|
97
|
+
const editInTerminal = async (initial) => {
|
|
98
|
+
if (services?.edit)
|
|
99
|
+
return services.edit(initial);
|
|
100
|
+
if (isRawModeSupported)
|
|
101
|
+
setRawMode(false);
|
|
102
|
+
try {
|
|
103
|
+
return await composeInEditor(initial);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (isRawModeSupported)
|
|
107
|
+
setRawMode(true);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
52
110
|
// Per-turn AbortController so Escape can cancel a running agent turn (see
|
|
53
111
|
// the useInput handler below). Stored in a ref so it survives re-renders
|
|
54
112
|
// across the async run and stays reachable from the key handler.
|
|
@@ -60,7 +118,9 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
60
118
|
const redactionsRef = useRef(0);
|
|
61
119
|
const handleSubmit = useCallback(async (raw) => {
|
|
62
120
|
const task = raw.trim();
|
|
63
|
-
|
|
121
|
+
// No `running` check: submitFromUser queues what arrives mid-turn, and this callback's
|
|
122
|
+
// `running` can be a render behind when a queued message or a /load line comes in.
|
|
123
|
+
if (!task)
|
|
64
124
|
return;
|
|
65
125
|
setInput('');
|
|
66
126
|
// Slash commands
|
|
@@ -73,7 +133,7 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
73
133
|
historyRef.current = [];
|
|
74
134
|
return;
|
|
75
135
|
}
|
|
76
|
-
if (task.startsWith('/save')) {
|
|
136
|
+
if (task === '/save' || task.startsWith('/save ')) {
|
|
77
137
|
const label = task.replace(/^\/save\s*/, '').trim();
|
|
78
138
|
if (label)
|
|
79
139
|
setCurrentLabel(label);
|
|
@@ -103,23 +163,9 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
103
163
|
return;
|
|
104
164
|
}
|
|
105
165
|
if (task === '/help') {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
kind: 'status',
|
|
110
|
-
id: cryptoId(),
|
|
111
|
-
text: 'Slash commands: /help /clear /reset /save [label] /plan <task> /spec <task> /compact /rewind /ok /model /weak-model /models /add /read-only /drop /ls /run /test /lint /tokens /settings /commit /undo /diff /map /map-refresh /exit /quit.\n' +
|
|
112
|
-
'/commit commits all pending changes with a drafted message; /undo reverts the last Forge commit; /diff shows uncommitted changes.\n' +
|
|
113
|
-
'/model [id|alias|provider:model] switches (or shows) the model; /weak-model sets the cheap model; /models [query] lists aliases and known models.\n' +
|
|
114
|
-
'/add <files> and /read-only <files> attach files to every turn; /drop [files] detaches; /ls lists them. /run <cmd> (or !<cmd>) runs a command and shows the model its output on the next turn; /test [cmd] and /lint [files] send failures to the model to fix.\n' +
|
|
115
|
-
'/ok says "go ahead"; /reset detaches files and clears the conversation; /tokens shows context use; /settings shows the effective settings.\n' +
|
|
116
|
-
'/map shows the repo map (--map-tokens sets the size sent to the model); /map-refresh re-parses every file first.\n' +
|
|
117
|
-
`Otherwise type a task and press Enter.\n` +
|
|
118
|
-
`Session: ${currentSessionId} (auto-saved after each turn).\n` +
|
|
119
|
-
'Plan review: when a plan is proposed, type a (approve), e (edit), or c (cancel).\n' +
|
|
120
|
-
'Spec review: same a/e/c keys: approve writes requirements.md/design.md/tasks.md to .forge/specs/<id>/.',
|
|
121
|
-
},
|
|
122
|
-
]);
|
|
166
|
+
const custom = await loadCustomCommands(agentConfigRef.current.cwd ?? process.cwd()).catch(() => new Map());
|
|
167
|
+
const extra = custom.size > 0 ? `\nYour commands:\n${formatCustomCommands(custom)}` : '';
|
|
168
|
+
setMessages((m) => [...m, { kind: 'status', id: cryptoId(), text: replHelpText(currentSessionId) + extra }]);
|
|
123
169
|
return;
|
|
124
170
|
}
|
|
125
171
|
if (task === '/id') {
|
|
@@ -204,12 +250,15 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
204
250
|
if (action === 'a' || action === 'approve') {
|
|
205
251
|
const approvedPlan = pendingPlan;
|
|
206
252
|
const approvedTask = pendingPlanTask ?? task;
|
|
253
|
+
const applyWith = architectEditorRef.current;
|
|
254
|
+
architectEditorRef.current = null;
|
|
207
255
|
setPendingPlan(null);
|
|
208
256
|
setPendingPlanTask(null);
|
|
209
|
-
await runWithPlan(approvedTask, approvedPlan);
|
|
257
|
+
await runWithPlan(approvedTask, approvedPlan, applyWith ?? undefined);
|
|
210
258
|
return;
|
|
211
259
|
}
|
|
212
260
|
if (action === 'c' || action === 'cancel') {
|
|
261
|
+
architectEditorRef.current = null;
|
|
213
262
|
setPendingPlan(null);
|
|
214
263
|
setPendingPlanTask(null);
|
|
215
264
|
setMessages((m) => [
|
|
@@ -331,7 +380,7 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
331
380
|
]);
|
|
332
381
|
return;
|
|
333
382
|
}
|
|
334
|
-
if (task.startsWith('/plan')) {
|
|
383
|
+
if (task === '/plan' || task.startsWith('/plan ')) {
|
|
335
384
|
const planTask = task.replace(/^\/plan\s*/, '').trim();
|
|
336
385
|
if (!planTask) {
|
|
337
386
|
setMessages((m) => [
|
|
@@ -343,7 +392,7 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
343
392
|
await runInPlanMode(planTask);
|
|
344
393
|
return;
|
|
345
394
|
}
|
|
346
|
-
if (task.startsWith('/spec')) {
|
|
395
|
+
if (task === '/spec' || task.startsWith('/spec ')) {
|
|
347
396
|
const specTask = task.replace(/^\/spec\s*/, '').trim();
|
|
348
397
|
if (!specTask) {
|
|
349
398
|
setMessages((m) => [
|
|
@@ -360,10 +409,39 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
360
409
|
return;
|
|
361
410
|
}
|
|
362
411
|
const slash = parseSlashCommand(task);
|
|
363
|
-
if (slash && ['model', 'weak-model', 'models', 'run', 'test', 'lint', 'ok', 'add', 'read-only', 'drop', 'ls', 'reset', 'tokens', 'settings'].includes(slash.name)) {
|
|
412
|
+
if (slash && ['model', 'weak-model', 'models', 'run', 'test', 'lint', 'ok', 'add', 'read-only', 'drop', 'ls', 'reset', 'tokens', 'settings', 'help', 'copy', 'copy-context', 'paste', 'web', 'editor', 'edit', 'report', 'reasoning-effort', 'think-tokens', 'editor-model', 'architect', 'load', 'save-script'].includes(slash.name)) {
|
|
364
413
|
const say = (text, isError) => setMessages((m) => [...m, { kind: 'status', id: cryptoId(), text, ...(isError ? { isError } : {}) }]);
|
|
365
414
|
const cwd = agentConfigRef.current.cwd ?? process.cwd();
|
|
366
415
|
const { name, args } = slash;
|
|
416
|
+
/** Run one command, showing the command and its output tail. */
|
|
417
|
+
const runShown = async (command) => {
|
|
418
|
+
say(`$ ${command}`);
|
|
419
|
+
setRunning(true);
|
|
420
|
+
setStatus('running');
|
|
421
|
+
let result;
|
|
422
|
+
try {
|
|
423
|
+
result = await runShell(command, cwd);
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
setRunning(false);
|
|
427
|
+
setStatus('idle');
|
|
428
|
+
}
|
|
429
|
+
const tail = result.output.trim().split('\n').slice(-40).join('\n');
|
|
430
|
+
say(`${tail || '(no output)'}\n[${result.timedOut ? 'timed out' : `exit ${result.exitCode ?? 'none'}`}]`, result.exitCode !== 0);
|
|
431
|
+
return result;
|
|
432
|
+
};
|
|
433
|
+
/** Run checks in order; the first failure goes to the model to fix, as in Aider. */
|
|
434
|
+
const runChecks = async (kind, commands) => {
|
|
435
|
+
for (const command of commands) {
|
|
436
|
+
const result = await runShown(command);
|
|
437
|
+
if (result.exitCode === 0)
|
|
438
|
+
continue;
|
|
439
|
+
const fix = fixTask(kind, command, result);
|
|
440
|
+
setMessages((m) => [...m, { kind: 'user', id: cryptoId(), text: fix }]);
|
|
441
|
+
await runAgent(fix, { planMode: false });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
367
445
|
try {
|
|
368
446
|
if (name === 'model' || name === 'weak-model') {
|
|
369
447
|
const weak = name === 'weak-model';
|
|
@@ -394,6 +472,174 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
394
472
|
say(formatModels(aliases, args));
|
|
395
473
|
return;
|
|
396
474
|
}
|
|
475
|
+
if (name === 'reasoning-effort' || name === 'think-tokens') {
|
|
476
|
+
if (!args) {
|
|
477
|
+
say(`reasoning effort: ${model.reasoningEffort ?? 'default (the provider decides)'}\n` +
|
|
478
|
+
(name === 'reasoning-effort' ? 'usage: /reasoning-effort <none|minimal|low|medium|high|xhigh|default>' : 'usage: /think-tokens <tokens, e.g. 8k | 0 | default>'));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const change = name === 'reasoning-effort' ? parseReasoningEffort(args) : parseThinkTokens(args);
|
|
482
|
+
if (!change.ok) {
|
|
483
|
+
say(change.error, true);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const { reasoningEffort: _dropped, ...rest } = model;
|
|
487
|
+
const next = change.effort ? { ...rest, reasoningEffort: change.effort } : rest;
|
|
488
|
+
setModel(next);
|
|
489
|
+
agentConfigRef.current = {
|
|
490
|
+
...agentConfigRef.current,
|
|
491
|
+
model: next,
|
|
492
|
+
...(weakModel ? { modelRouter: createExploreEditRouter({ strong: next, cheap: weakModel }) } : {}),
|
|
493
|
+
};
|
|
494
|
+
const note = 'note' in change && typeof change.note === 'string' ? change.note : undefined;
|
|
495
|
+
say(note ?? `reasoning effort → ${change.effort ?? 'default (the provider decides)'}`);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (name === 'editor-model') {
|
|
499
|
+
if (!args) {
|
|
500
|
+
say(`editor model: ${editorModel ? describeModel(editorModel) : '(none) set one with /editor-model <model>, then use /architect <task>'}`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (args.toLowerCase() === 'off') {
|
|
504
|
+
setEditorModel(undefined);
|
|
505
|
+
say('editor model cleared');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const res = switchModel(args, model, aliases);
|
|
509
|
+
if (!res.ok) {
|
|
510
|
+
say(res.error, true);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
setEditorModel(res.model);
|
|
514
|
+
say(`editor model → ${describeModel(res.model)} (applies plans made with /architect)`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (name === 'architect') {
|
|
518
|
+
if (!editorModel) {
|
|
519
|
+
say('set the model that applies the plan first: /editor-model <model>', true);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (!args) {
|
|
523
|
+
say('usage: /architect <task> (the main model plans, you approve, then the editor model applies it)', true);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
architectEditorRef.current = editorModel;
|
|
527
|
+
setMessages((m) => [...m, { kind: 'user', id: cryptoId(), text: `/architect ${args}` }]);
|
|
528
|
+
await runAgent(args, { planMode: true });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (name === 'load') {
|
|
532
|
+
if (!args) {
|
|
533
|
+
say('usage: /load <file> (one command or message per line; blank lines and # comments are skipped)', true);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (loadingRef.current) {
|
|
537
|
+
say('a script cannot /load another script', true);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const file = path.resolve(cwd, args);
|
|
541
|
+
let text;
|
|
542
|
+
try {
|
|
543
|
+
text = await readFile(file, 'utf8');
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
say(`cannot read ${args}`, true);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const lines = scriptLines(text);
|
|
550
|
+
if (lines.length === 0) {
|
|
551
|
+
say(`${args} has no commands`, true);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (lines.length > 500) {
|
|
555
|
+
say(`${args} has ${lines.length} lines; scripts are limited to 500`, true);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
loadingRef.current = true;
|
|
559
|
+
try {
|
|
560
|
+
for (const line of lines) {
|
|
561
|
+
say(`> ${line}`);
|
|
562
|
+
await submitRef.current?.(line);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
loadingRef.current = false;
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (name === 'save-script') {
|
|
571
|
+
if (!args) {
|
|
572
|
+
say('usage: /save-script <file> (writes the model and attached files as commands for /load)', true);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const target = path.resolve(cwd, args);
|
|
576
|
+
const rel = path.relative(cwd, target);
|
|
577
|
+
if (rel.startsWith('..') || path.isAbsolute(rel) || rel === '') {
|
|
578
|
+
say('the script must be inside the working directory', true);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const script = formatScript({ model, weakModel, editorModel, files: chatFilesRef.current.list() });
|
|
582
|
+
await writeFile(target, script, 'utf8');
|
|
583
|
+
say(`wrote ${scriptLines(script).length} commands to ${rel.split(path.sep).join('/')}`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (name === 'help') {
|
|
587
|
+
// /help <question>: answered from the command reference; read-only tools only.
|
|
588
|
+
const reference = `${replHelpText(currentSessionId)}\n\nCommand-line options:\n${buildProgram().helpInformation()}`;
|
|
589
|
+
const question = helpQuestionTask(args, reference);
|
|
590
|
+
setMessages((m) => [...m, { kind: 'user', id: cryptoId(), text: `/help ${args}` }]);
|
|
591
|
+
await runAgent(question, { planMode: false, readOnlyTools: true });
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (name === 'copy' || name === 'copy-context') {
|
|
595
|
+
const text = name === 'copy' ? lastAssistantText(historyRef.current) : conversationMarkdown(historyRef.current);
|
|
596
|
+
if (!text) {
|
|
597
|
+
say(name === 'copy' ? 'nothing to copy yet: no reply from the model' : 'nothing to copy yet: the conversation is empty', true);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const res = await (services?.copy ?? copyToClipboard)(text);
|
|
601
|
+
say(res.ok ? `copied ${text.length.toLocaleString('en-US')} characters to the clipboard (${res.tool})` : `copy failed: ${res.error}`, !res.ok);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (name === 'paste') {
|
|
605
|
+
const res = await (services?.read ?? readClipboard)();
|
|
606
|
+
if (!res.ok) {
|
|
607
|
+
say(`paste failed: ${res.error}`, true);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (!res.text.trim()) {
|
|
611
|
+
say('the clipboard is empty', true);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
pendingNotesRef.current.push(`Pasted from the clipboard:\n\`\`\`\n${redact(res.text.trimEnd()).text}\n\`\`\``);
|
|
615
|
+
say(`pasted ${res.text.length.toLocaleString('en-US')} characters (sent with your next message)`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (name === 'web') {
|
|
619
|
+
if (!args) {
|
|
620
|
+
say('usage: /web <url>', true);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
say(`fetching ${args} ...`);
|
|
624
|
+
const page = await (services?.fetchPage ?? defaultFetchPage)(args, cwd);
|
|
625
|
+
pendingNotesRef.current.push(`Web page ${page.url}${page.title ? ` ("${page.title}")` : ''}${page.truncated ? ' (truncated)' : ''}:\n\`\`\`\n${page.excerpt}\n\`\`\``);
|
|
626
|
+
say(`fetched ${page.url} (HTTP ${page.status}, ${page.excerpt.length.toLocaleString('en-US')} characters${page.truncated ? ', truncated' : ''}); sent with your next message`);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (name === 'editor' || name === 'edit') {
|
|
630
|
+
const res = await editInTerminal(args);
|
|
631
|
+
if (!res.ok) {
|
|
632
|
+
say(`/${name}: ${res.error}`, true);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
await chatTurn(res.text, res.text);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (name === 'report') {
|
|
639
|
+
const url = reportUrl({ version, platform: `${process.platform} ${process.arch}`, node: process.version, model, sessionId: currentSessionId });
|
|
640
|
+
say(`Open this link to file an issue. It is prefilled with diagnostics only (version, platform, model): no keys, paths or prompts.\n${url}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
397
643
|
if (name === 'ls') {
|
|
398
644
|
const { editable, readOnly } = chatFilesRef.current.list();
|
|
399
645
|
say(editable.length + readOnly.length === 0
|
|
@@ -454,6 +700,10 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
454
700
|
await runAgent(OK_TASK, { planMode: false });
|
|
455
701
|
return;
|
|
456
702
|
}
|
|
703
|
+
if (name === 'lint' && !args && lintCmds.length > 0) {
|
|
704
|
+
await runChecks('lint', lintCmds);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
457
707
|
if (name === 'lint') {
|
|
458
708
|
const aut = agentConfigRef.current.autoLint ?? { syntax: true };
|
|
459
709
|
const named = args ? args.split(/\s+/).filter(Boolean) : [];
|
|
@@ -486,35 +736,26 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
486
736
|
await runAgent(fix, { planMode: false });
|
|
487
737
|
return;
|
|
488
738
|
}
|
|
489
|
-
// /run
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
739
|
+
// /run runs one command; /test with no argument runs every --test-cmd.
|
|
740
|
+
if (name === 'test' && !args) {
|
|
741
|
+
if (testCmds.length === 0) {
|
|
742
|
+
say('usage: /test <command> (or start forge with --test-cmd <command>)', true);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
await runChecks('test', testCmds);
|
|
493
746
|
return;
|
|
494
747
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
setStatus('running');
|
|
498
|
-
let result;
|
|
499
|
-
try {
|
|
500
|
-
result = await runShell(command, cwd);
|
|
501
|
-
}
|
|
502
|
-
finally {
|
|
503
|
-
setRunning(false);
|
|
504
|
-
setStatus('idle');
|
|
505
|
-
}
|
|
506
|
-
const tail = result.output.trim().split('\n').slice(-40).join('\n');
|
|
507
|
-
say(`${tail || '(no output)'}\n[${result.timedOut ? 'timed out' : `exit ${result.exitCode ?? 'none'}`}]`, result.exitCode !== 0);
|
|
508
|
-
if (name === 'run') {
|
|
509
|
-
pendingNotesRef.current.push(runContextNote(command, result));
|
|
510
|
-
say('(output will be sent to the model with your next message)');
|
|
748
|
+
if (!args) {
|
|
749
|
+
say(name === 'test' ? 'usage: /test <command> (or start forge with --test-cmd <command>)' : 'usage: /run <command> (or !<command>)', true);
|
|
511
750
|
return;
|
|
512
751
|
}
|
|
513
|
-
if (
|
|
752
|
+
if (name === 'test') {
|
|
753
|
+
await runChecks('test', [args]);
|
|
514
754
|
return;
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
755
|
+
}
|
|
756
|
+
const result = await runShown(args);
|
|
757
|
+
pendingNotesRef.current.push(runContextNote(args, result));
|
|
758
|
+
say('(output will be sent to the model with your next message)');
|
|
518
759
|
}
|
|
519
760
|
catch (err) {
|
|
520
761
|
setRunning(false);
|
|
@@ -538,17 +779,6 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
538
779
|
}
|
|
539
780
|
return;
|
|
540
781
|
}
|
|
541
|
-
if (task === '/tokens') {
|
|
542
|
-
setMessages((m) => [
|
|
543
|
-
...m,
|
|
544
|
-
{
|
|
545
|
-
kind: 'status',
|
|
546
|
-
id: cryptoId(),
|
|
547
|
-
text: `tokens this session: ${inputTokens} in, ${outputTokens} out${costUsd !== undefined ? `, $${costUsd.toFixed(4)}` : ''}`,
|
|
548
|
-
},
|
|
549
|
-
]);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
782
|
if (task === '/commit' || task === '/undo' || task === '/diff') {
|
|
553
783
|
const say = (text, isError) => setMessages((m) => [...m, { kind: 'status', id: cryptoId(), text, ...(isError ? { isError } : {}) }]);
|
|
554
784
|
const cwd = agentConfigRef.current.cwd ?? process.cwd();
|
|
@@ -583,9 +813,57 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
583
813
|
}
|
|
584
814
|
return;
|
|
585
815
|
}
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
816
|
+
const say = (text, isError) => setMessages((m) => [...m, { kind: 'status', id: cryptoId(), text, ...(isError ? { isError } : {}) }]);
|
|
817
|
+
if (task === '/retry' || task.startsWith('/retry ') || task === '/edit-last') {
|
|
818
|
+
const last = lastTurnRef.current;
|
|
819
|
+
if (!last) {
|
|
820
|
+
say('nothing to retry yet: send a message first', true);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
let next = task.replace(/^\/retry\s*/, '').trim() || last.task;
|
|
824
|
+
if (task === '/edit-last') {
|
|
825
|
+
const res = await editInTerminal(last.task);
|
|
826
|
+
if (!res.ok) {
|
|
827
|
+
say(`/edit-last: ${res.error}`, true);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
next = res.text;
|
|
831
|
+
}
|
|
832
|
+
// Drop the last turn from the conversation and run the message again in its place.
|
|
833
|
+
historyRef.current = last.historyBefore;
|
|
834
|
+
say(`retrying${next !== last.task ? ' with your edit' : ''}: the last turn is dropped from the conversation (files it changed stay changed; /undo or /rewind reverts them)`);
|
|
835
|
+
await chatTurn(next, next);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (task === '/sessions' || task.startsWith('/sessions ')) {
|
|
839
|
+
const query = task.replace(/^\/sessions\s*/, '').trim();
|
|
840
|
+
try {
|
|
841
|
+
if (!query) {
|
|
842
|
+
const recent = (await listSessions(sessionDir)).slice(0, 10);
|
|
843
|
+
say(recent.length === 0
|
|
844
|
+
? 'no saved sessions yet'
|
|
845
|
+
: `recent sessions (resume with forge --resume <id>):\n${recent.map((e) => ` ${e.id}${e.label ? ` [${e.label}]` : ''} ${e.updatedAt.slice(0, 16).replace('T', ' ')} ${e.preview ?? ''}`).join('\n')}`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const found = await searchSessions(sessionDir, query, { limit: 10 });
|
|
849
|
+
say(found.length === 0
|
|
850
|
+
? `no session mentions "${query}"`
|
|
851
|
+
: `sessions mentioning "${query}" (resume with forge --resume <id>):\n${found.map((f) => ` ${f.entry.id}${f.entry.label ? ` [${f.entry.label}]` : ''} ${f.entry.updatedAt.slice(0, 10)} ${f.snippet}`).join('\n')}`);
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
say(`/sessions: ${err.message}`, true);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const customSlash = parseSlashCommand(task);
|
|
859
|
+
if (customSlash && !task.startsWith('!')) {
|
|
860
|
+
const custom = (await loadCustomCommands(agentConfigRef.current.cwd ?? process.cwd()).catch(() => new Map())).get(customSlash.name);
|
|
861
|
+
if (custom) {
|
|
862
|
+
await chatTurn(task, expandCustomCommand(custom, customSlash.args));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
await chatTurn(task, task);
|
|
589
867
|
}, [
|
|
590
868
|
running,
|
|
591
869
|
onExit,
|
|
@@ -597,13 +875,63 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
597
875
|
sessionDir,
|
|
598
876
|
costUsd,
|
|
599
877
|
pendingPlan,
|
|
878
|
+
pendingPlanTask,
|
|
600
879
|
pendingSpec,
|
|
880
|
+
pendingSpecTask,
|
|
881
|
+
// Everything the handler reads from props or state: a value missing here is stale after it changes.
|
|
882
|
+
weakModel,
|
|
883
|
+
editorModel,
|
|
884
|
+
aliases,
|
|
885
|
+
services,
|
|
886
|
+
version,
|
|
887
|
+
lintCmds,
|
|
888
|
+
testCmds,
|
|
889
|
+
gitWorkflow,
|
|
890
|
+
gitOptions,
|
|
891
|
+
commitModel,
|
|
601
892
|
]);
|
|
893
|
+
submitRef.current = handleSubmit;
|
|
894
|
+
/** One chat message: shown as `shown`, sent as `task`, and remembered for /retry. */
|
|
895
|
+
async function chatTurn(shown, task) {
|
|
896
|
+
lastTurnRef.current = { task, historyBefore: historyRef.current };
|
|
897
|
+
setMessages((m) => [...m, { kind: 'user', id: cryptoId(), text: shown }]);
|
|
898
|
+
await runAgent(task, { planMode: false });
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* What Enter does: handle the message now, or queue it while an earlier one is still being
|
|
902
|
+
* handled (a turn, a /test run, a script). Queued messages then run one by one, in order.
|
|
903
|
+
*/
|
|
904
|
+
const submitFromUser = (raw) => {
|
|
905
|
+
const task = raw.trim();
|
|
906
|
+
if (!task)
|
|
907
|
+
return;
|
|
908
|
+
if (inFlightRef.current) {
|
|
909
|
+
queueRef.current.push(task);
|
|
910
|
+
setInput('');
|
|
911
|
+
setMessages((m) => [
|
|
912
|
+
...m,
|
|
913
|
+
{ kind: 'status', id: cryptoId(), text: `queued (${queueRef.current.length} waiting): ${task.length > 60 ? `${task.slice(0, 57)}...` : task}` },
|
|
914
|
+
]);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
inFlightRef.current = true;
|
|
918
|
+
void (async () => {
|
|
919
|
+
try {
|
|
920
|
+
await submitRef.current?.(task);
|
|
921
|
+
}
|
|
922
|
+
finally {
|
|
923
|
+
inFlightRef.current = false;
|
|
924
|
+
const next = queueRef.current.shift();
|
|
925
|
+
if (next !== undefined)
|
|
926
|
+
submitFromUser(next);
|
|
927
|
+
}
|
|
928
|
+
})();
|
|
929
|
+
};
|
|
602
930
|
/**
|
|
603
931
|
* Run the agent for a single turn. `options.planMode: true` makes the
|
|
604
932
|
* agent emit a plan instead of executing; the returned finalText holds
|
|
605
|
-
* the plan. `options.planContext`
|
|
606
|
-
*
|
|
933
|
+
* the plan. `options.planContext` puts an `<approved-plan>` block at the
|
|
934
|
+
* start of this turn's message so the agent knows what to do.
|
|
607
935
|
*
|
|
608
936
|
* Declared as a regular function (not `useCallback`) so it can be
|
|
609
937
|
* referenced by `handleSubmit` below: function declarations are hoisted
|
|
@@ -616,19 +944,15 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
616
944
|
abortControllerRef.current = controller;
|
|
617
945
|
const approvals = new Map();
|
|
618
946
|
const overrides = {
|
|
947
|
+
// An /architect plan is applied by the editor model; its cost router must not route back to the planner.
|
|
948
|
+
...(options.modelOverride ? { model: options.modelOverride, modelRouter: undefined } : {}),
|
|
619
949
|
planMode: options.planMode,
|
|
620
950
|
specMode: options.specMode === true,
|
|
621
|
-
...(options.planContext
|
|
622
|
-
? {
|
|
623
|
-
systemPrompt: (agentConfigRef.current.systemPrompt ?? '') +
|
|
624
|
-
`\n\n<approved-plan>\n${options.planContext}\n</approved-plan>\nFollow the plan above. You may deviate if you find an issue, but explain why in your final summary.`,
|
|
625
|
-
}
|
|
626
|
-
: {}),
|
|
627
951
|
// Spec mode always gets exactly the six read-only tools: a hard
|
|
628
952
|
// override of whatever toolSandbox the user configured via
|
|
629
953
|
// --allow-tools/--deny-tools, matching options.ts's toAgentConfig
|
|
630
954
|
// wiring for one-shot `--spec`.
|
|
631
|
-
...(options.specMode === true ? { toolSandbox: { allowTools: [...SPEC_READONLY_TOOLS] } } : {}),
|
|
955
|
+
...(options.specMode === true || options.readOnlyTools === true ? { toolSandbox: { allowTools: [...SPEC_READONLY_TOOLS] } } : {}),
|
|
632
956
|
};
|
|
633
957
|
const localConfig = {
|
|
634
958
|
...agentConfigRef.current,
|
|
@@ -644,7 +968,12 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
644
968
|
const attached = await chatFilesRef.current.render();
|
|
645
969
|
for (const note of attached.notes)
|
|
646
970
|
setMessages((m) => [...m, { kind: 'status', id: cryptoId(), text: note }]);
|
|
647
|
-
|
|
971
|
+
// The approved plan rides in this turn's message: a system-prompt override is ignored once the
|
|
972
|
+
// conversation has history (and it always has, right after the plan run), which also dropped edited plans.
|
|
973
|
+
const planBlock = options.planContext
|
|
974
|
+
? `<approved-plan>\n${options.planContext}\n</approved-plan>\nFollow the plan above. You may deviate if you find an issue, but explain why in your final summary.`
|
|
975
|
+
: '';
|
|
976
|
+
const turnContext = [planBlock, ...pendingNotesRef.current.splice(0), attached.block].filter((part) => part.length > 0).join('\n\n');
|
|
648
977
|
if (localConfig.repoMap && chatFilesRef.current.size > 0) {
|
|
649
978
|
localConfig.repoMap = { ...localConfig.repoMap, focusFiles: chatFilesRef.current.focusFiles() };
|
|
650
979
|
}
|
|
@@ -661,6 +990,7 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
661
990
|
agentRunResult = await agent.run({
|
|
662
991
|
task,
|
|
663
992
|
...(turnContext ? { context: turnContext } : {}),
|
|
993
|
+
...(attached.attachments.length > 0 ? { attachments: attached.attachments } : {}),
|
|
664
994
|
history: historyRef.current,
|
|
665
995
|
abortSignal: controller.signal,
|
|
666
996
|
onEvent: (event) => {
|
|
@@ -907,9 +1237,8 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
907
1237
|
}
|
|
908
1238
|
if (agentRunResult) {
|
|
909
1239
|
// The attached files were for this turn; keep them out of the stored history.
|
|
910
|
-
historyRef.current =
|
|
911
|
-
|
|
912
|
-
: agentRunResult.messages;
|
|
1240
|
+
historyRef.current =
|
|
1241
|
+
attached.block || attached.attachments.length > 0 ? agentRunResult.messages.map(stripAttachedFiles) : agentRunResult.messages;
|
|
913
1242
|
if (historyRef.current.length > 0 && !options.planMode && !options.specMode) {
|
|
914
1243
|
const savedPath = await persistSession({
|
|
915
1244
|
id: currentSessionId,
|
|
@@ -940,17 +1269,18 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
940
1269
|
}
|
|
941
1270
|
/** Run the agent in plan mode for a task. */
|
|
942
1271
|
function runInPlanMode(planTask) {
|
|
1272
|
+
architectEditorRef.current = null; // a plain /plan is applied by the main model
|
|
943
1273
|
setMessages((m) => [...m, { kind: 'user', id: cryptoId(), text: `/plan ${planTask}` }]);
|
|
944
1274
|
return runAgent(planTask, { planMode: true });
|
|
945
1275
|
}
|
|
946
1276
|
/** Run a normal (non-plan) agent turn with a previously-approved plan injected. */
|
|
947
|
-
function runWithPlan(realTask, plan) {
|
|
1277
|
+
function runWithPlan(realTask, plan, applyWith) {
|
|
948
1278
|
setMessages((m) => [
|
|
949
1279
|
...m,
|
|
950
|
-
{ kind: 'status', id: cryptoId(), text: '✓ plan approved: executing…' },
|
|
1280
|
+
{ kind: 'status', id: cryptoId(), text: applyWith ? `✓ plan approved: ${describeModel(applyWith)} is applying it…` : '✓ plan approved: executing…' },
|
|
951
1281
|
{ kind: 'user', id: cryptoId(), text: realTask },
|
|
952
1282
|
]);
|
|
953
|
-
return runAgent(realTask, { planMode: false, planContext: plan });
|
|
1283
|
+
return runAgent(realTask, { planMode: false, planContext: plan, ...(applyWith ? { modelOverride: applyWith } : {}) });
|
|
954
1284
|
}
|
|
955
1285
|
/**
|
|
956
1286
|
* Run the agent in spec mode for a task. Unlike `runWithPlan`, there is
|
|
@@ -1043,9 +1373,12 @@ export function Repl({ baseConfig, model: initialModel, onExit, sessionId, initi
|
|
|
1043
1373
|
pendingApproval && (React.createElement(Approval, { request: pendingApproval.request, onResolve: handleApproval }))),
|
|
1044
1374
|
React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, flexDirection: "row" },
|
|
1045
1375
|
React.createElement(Text, { color: "cyan", bold: true }, "forge \u276F"),
|
|
1046
|
-
React.createElement(Box, { marginLeft: 1, flexGrow: 1 },
|
|
1376
|
+
React.createElement(Box, { marginLeft: 1, flexGrow: 1 }, pendingApproval ? (React.createElement(Box, null,
|
|
1377
|
+
React.createElement(Spinner, null),
|
|
1378
|
+
React.createElement(Text, { color: "gray" }, " waiting for your answer above"))) : running ? (React.createElement(Box, null,
|
|
1047
1379
|
React.createElement(Spinner, null),
|
|
1048
|
-
React.createElement(Text, { color: "gray" }, " running\u2026 (esc to cancel)
|
|
1380
|
+
React.createElement(Text, { color: "gray" }, " running\u2026 (esc to cancel) "),
|
|
1381
|
+
React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: submitFromUser, placeholder: "type to queue a message" }))) : (React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: submitFromUser, placeholder: "ask forge anything\u2026" })))),
|
|
1049
1382
|
React.createElement(Box, { marginTop: 1 },
|
|
1050
1383
|
React.createElement(Text, { color: "gray" },
|
|
1051
1384
|
"\u2191",
|