deepclause-sdk 0.1.2 → 0.2.1
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 +45 -6
- package/dist/cli/config.d.ts +2 -2
- package/dist/cli/repl.d.ts +55 -4
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +675 -52
- package/dist/cli/repl.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +516 -2
- package/dist/repl/backend.d.ts +9 -0
- package/dist/repl/backend.d.ts.map +1 -1
- package/dist/repl/backend.js +62 -5
- package/dist/repl/backend.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +36 -0
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +173 -0
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +29 -0
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +71 -0
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +6 -0
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -1
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/picker.d.ts +3 -0
- package/dist/repl/picker.d.ts.map +1 -1
- package/dist/repl/picker.js +132 -0
- package/dist/repl/picker.js.map +1 -1
- package/dist/repl/process-backend.d.ts +31 -0
- package/dist/repl/process-backend.d.ts.map +1 -0
- package/dist/repl/process-backend.js +10 -0
- package/dist/repl/process-backend.js.map +1 -0
- package/dist/repl/process-executor.d.ts +67 -0
- package/dist/repl/process-executor.d.ts.map +1 -0
- package/dist/repl/process-executor.js +371 -0
- package/dist/repl/process-executor.js.map +1 -0
- package/dist/repl/process-worker.d.ts +10 -0
- package/dist/repl/process-worker.d.ts.map +1 -0
- package/dist/repl/process-worker.js +210 -0
- package/dist/repl/process-worker.js.map +1 -0
- package/dist/repl/processes.d.ts +282 -0
- package/dist/repl/processes.d.ts.map +1 -0
- package/dist/repl/processes.js +557 -0
- package/dist/repl/processes.js.map +1 -0
- package/dist/repl/render.d.ts +52 -2
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +52 -17
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +135 -3
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +413 -4
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/status.d.ts +3 -1
- package/dist/repl/status.d.ts.map +1 -1
- package/dist/repl/status.js +4 -0
- package/dist/repl/status.js.map +1 -1
- package/dist/repl/tui/autocomplete.d.ts +19 -0
- package/dist/repl/tui/autocomplete.d.ts.map +1 -0
- package/dist/repl/tui/autocomplete.js +48 -0
- package/dist/repl/tui/autocomplete.js.map +1 -0
- package/dist/repl/tui/document.d.ts +51 -0
- package/dist/repl/tui/document.d.ts.map +1 -0
- package/dist/repl/tui/document.js +160 -0
- package/dist/repl/tui/document.js.map +1 -0
- package/dist/repl/tui/harness-view.d.ts +102 -0
- package/dist/repl/tui/harness-view.d.ts.map +1 -0
- package/dist/repl/tui/harness-view.js +323 -0
- package/dist/repl/tui/harness-view.js.map +1 -0
- package/dist/repl/tui/picker.d.ts +31 -0
- package/dist/repl/tui/picker.d.ts.map +1 -0
- package/dist/repl/tui/picker.js +43 -0
- package/dist/repl/tui/picker.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts +98 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -0
- package/dist/repl/tui/renderer.js +289 -0
- package/dist/repl/tui/renderer.js.map +1 -0
- package/dist/repl/tui/services-panel.d.ts +35 -0
- package/dist/repl/tui/services-panel.d.ts.map +1 -0
- package/dist/repl/tui/services-panel.js +108 -0
- package/dist/repl/tui/services-panel.js.map +1 -0
- package/dist/repl/tui/status-bar.d.ts +6 -2
- package/dist/repl/tui/status-bar.d.ts.map +1 -1
- package/dist/repl/tui/status-bar.js +82 -24
- package/dist/repl/tui/status-bar.js.map +1 -1
- package/dist/repl/tui/terminal.d.ts +28 -0
- package/dist/repl/tui/terminal.d.ts.map +1 -0
- package/dist/repl/tui/terminal.js +94 -0
- package/dist/repl/tui/terminal.js.map +1 -0
- package/dist/repl/tui/theme.d.ts +60 -0
- package/dist/repl/tui/theme.d.ts.map +1 -1
- package/dist/repl/tui/theme.js +142 -0
- package/dist/repl/tui/theme.js.map +1 -1
- package/dist/repl/tui/tool-pane.d.ts +68 -0
- package/dist/repl/tui/tool-pane.d.ts.map +1 -0
- package/dist/repl/tui/tool-pane.js +261 -0
- package/dist/repl/tui/tool-pane.js.map +1 -0
- package/dist/repl/tui/working-line.d.ts +23 -0
- package/dist/repl/tui/working-line.d.ts.map +1 -0
- package/dist/repl/tui/working-line.js +58 -0
- package/dist/repl/tui/working-line.js.map +1 -0
- package/dist/repl/worker-host.d.ts +80 -0
- package/dist/repl/worker-host.d.ts.map +1 -0
- package/dist/repl/worker-host.js +305 -0
- package/dist/repl/worker-host.js.map +1 -0
- package/dist/repl/worker-protocol.d.ts +123 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -0
- package/dist/repl/worker-protocol.js +9 -0
- package/dist/repl/worker-protocol.js.map +1 -0
- package/dist/runner.d.ts +24 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +47 -0
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +193 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +145 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/prolog-src/deepclause_mi.pl +516 -2
package/dist/cli/repl.js
CHANGED
|
@@ -14,14 +14,25 @@ import { createHarnessRepl } from '../repl/index.js';
|
|
|
14
14
|
import { HistoryStore } from '../repl/history.js';
|
|
15
15
|
import { resolveReplJudgeSelection } from '../repl/judge.js';
|
|
16
16
|
import { ReplRenderer } from '../repl/render.js';
|
|
17
|
+
import { TranscriptDocument } from '../repl/tui/document.js';
|
|
18
|
+
import { ServicesPanel } from '../repl/tui/services-panel.js';
|
|
19
|
+
import { formatProcessCode, formatProcessTable } from '../repl/harness-tools.js';
|
|
20
|
+
import { ChoicePicker } from '../repl/tui/picker.js';
|
|
21
|
+
import { openPiRuntime } from '../repl/backend.js';
|
|
22
|
+
import { listLoginProviders, loginToProvider } from '../repl/auth.js';
|
|
23
|
+
import { HarnessView } from '../repl/tui/harness-view.js';
|
|
24
|
+
import { TuiRenderer } from '../repl/tui/renderer.js';
|
|
25
|
+
import { WorkingLine } from '../repl/tui/working-line.js';
|
|
26
|
+
import { createReplAutocomplete } from '../repl/tui/autocomplete.js';
|
|
27
|
+
import { initTuiTheme, scrollbarStyles } from '../repl/tui/theme.js';
|
|
28
|
+
import { SizedTerminal } from '../repl/tui/terminal.js';
|
|
17
29
|
import { ReplStatusTracker } from '../repl/status.js';
|
|
18
|
-
import { LineSink, TranscriptComponent } from '../repl/tui/transcript.js';
|
|
19
30
|
import { StatusBarComponent } from '../repl/tui/status-bar.js';
|
|
20
|
-
import { Container,
|
|
31
|
+
import { Container, HStack, ScrollView, Spacer, TuiAltScreen, VStack, matchesKey, } from '@earendil-works/pi-tui';
|
|
21
32
|
import { editorTheme, formatInputAnswer } from '../repl/tui/theme.js';
|
|
22
33
|
import { PromptEditor } from '../repl/tui/editor.js';
|
|
23
34
|
import { loadProjectContext } from '../repl/project-context.js';
|
|
24
|
-
import { createPiBackend } from '../repl/backend.js';
|
|
35
|
+
import { createPiBackend, THINKING_LEVELS } from '../repl/backend.js';
|
|
25
36
|
import { createReplToolset } from '../repl/toolset.js';
|
|
26
37
|
import { takeStartupPrologMessages } from '../prolog/loader.js';
|
|
27
38
|
import { createPiImageBackend } from '../repl/imagegen.js';
|
|
@@ -71,6 +82,13 @@ export function parseVizArgs(arg) {
|
|
|
71
82
|
export function formatTurnInput(turn) {
|
|
72
83
|
return turn.mode === 'goal' ? `/goal ${turn.request}` : turn.request;
|
|
73
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Slash commands are executed (or queued), never steered into a running model
|
|
87
|
+
* turn: `/send`, `/call`, `/ps`, `/stop` … address processes, not the model.
|
|
88
|
+
*/
|
|
89
|
+
export function isSlashCommand(line) {
|
|
90
|
+
return line.trim().startsWith('/');
|
|
91
|
+
}
|
|
74
92
|
/**
|
|
75
93
|
* Classify one REPL input line.
|
|
76
94
|
*
|
|
@@ -113,6 +131,15 @@ export function classifyReplInput(line) {
|
|
|
113
131
|
return { kind: 'export', path: arg || undefined };
|
|
114
132
|
case 'model':
|
|
115
133
|
return { kind: 'model', model: arg || undefined };
|
|
134
|
+
case 'harness':
|
|
135
|
+
case 'side':
|
|
136
|
+
return { kind: 'harness', width: arg || undefined };
|
|
137
|
+
case 'reasoning':
|
|
138
|
+
case 'reasoning-level':
|
|
139
|
+
return { kind: 'reasoning', level: arg || undefined };
|
|
140
|
+
case 'show-reasoning':
|
|
141
|
+
case 'reasoning-summary':
|
|
142
|
+
return { kind: 'show-reasoning' };
|
|
116
143
|
case 'freeze':
|
|
117
144
|
return { kind: 'freeze' };
|
|
118
145
|
case 'unfreeze':
|
|
@@ -132,6 +159,36 @@ export function classifyReplInput(line) {
|
|
|
132
159
|
case 'gen':
|
|
133
160
|
case 'imagine':
|
|
134
161
|
return arg ? { kind: 'gen', prompt: arg } : { kind: 'unknown-command', name: 'gen (missing prompt)' };
|
|
162
|
+
case 'spawn':
|
|
163
|
+
case 'serve':
|
|
164
|
+
return arg ? { kind: 'spawn', goal: arg } : { kind: 'unknown-command', name: 'spawn (missing goal)' };
|
|
165
|
+
case 'ps':
|
|
166
|
+
case 'processes': {
|
|
167
|
+
const watch = /(?:^|\s)--watch(?:\s|$)/.test(arg);
|
|
168
|
+
const logs = /(?:^|\s)--logs(?:\s|$)/.test(arg);
|
|
169
|
+
// `--code` alone shows the current harness version; `--code <target>`
|
|
170
|
+
// prints the harness snapshot that process is running.
|
|
171
|
+
const codeMatch = /(?:^|\s)--code(?:=(\S+)|\s+(\S+))?/.exec(arg);
|
|
172
|
+
const code = codeMatch ? (codeMatch[1] ?? codeMatch[2] ?? '') : undefined;
|
|
173
|
+
return { kind: 'ps', watch, logs, code };
|
|
174
|
+
}
|
|
175
|
+
case 'send':
|
|
176
|
+
case 'cast': {
|
|
177
|
+
const [target, ...rest] = arg.split(/\s+/);
|
|
178
|
+
return target && rest.length > 0
|
|
179
|
+
? { kind: 'send', target, message: rest.join(' ') }
|
|
180
|
+
: { kind: 'unknown-command', name: 'send (usage: /send <name> <message>)' };
|
|
181
|
+
}
|
|
182
|
+
case 'call': {
|
|
183
|
+
const [target, ...rest] = arg.split(/\s+/);
|
|
184
|
+
return target && rest.length > 0
|
|
185
|
+
? { kind: 'call', target, message: rest.join(' ') }
|
|
186
|
+
: { kind: 'unknown-command', name: 'call (usage: /call <name> <message>)' };
|
|
187
|
+
}
|
|
188
|
+
case 'stop':
|
|
189
|
+
return arg ? { kind: 'stop', target: arg } : { kind: 'unknown-command', name: 'stop (usage: /stop <name>)' };
|
|
190
|
+
case 'login':
|
|
191
|
+
return { kind: 'login' };
|
|
135
192
|
default:
|
|
136
193
|
return { kind: 'unknown-command', name };
|
|
137
194
|
}
|
|
@@ -444,6 +501,7 @@ export async function createReplRuntime(options) {
|
|
|
444
501
|
modelResolver,
|
|
445
502
|
history: historyOptions,
|
|
446
503
|
harness: harnessName,
|
|
504
|
+
execution: options.execution,
|
|
447
505
|
autoLoadHarness: !(options.load?.length),
|
|
448
506
|
compaction: { settings: replCompaction, auto: replCompaction.auto },
|
|
449
507
|
contextWindow,
|
|
@@ -472,7 +530,7 @@ export async function createReplRuntime(options) {
|
|
|
472
530
|
+ "or run 'deepclause set-backend pi'.";
|
|
473
531
|
}
|
|
474
532
|
}
|
|
475
|
-
|
|
533
|
+
const runtime = {
|
|
476
534
|
repl,
|
|
477
535
|
renderer,
|
|
478
536
|
workspace,
|
|
@@ -484,6 +542,10 @@ export async function createReplRuntime(options) {
|
|
|
484
542
|
backend: backendKind,
|
|
485
543
|
contextWindow,
|
|
486
544
|
reasoning,
|
|
545
|
+
reasoningMode: options.showReasoning ?? config.repl.showReasoning,
|
|
546
|
+
modelResolver,
|
|
547
|
+
llmBackend,
|
|
548
|
+
setThinkingLevel: (level) => statusTracker.setThinkingLevel(level),
|
|
487
549
|
thinkingLevel,
|
|
488
550
|
sessionId: repl.session.getSessionId(),
|
|
489
551
|
harnessName,
|
|
@@ -495,6 +557,23 @@ export async function createReplRuntime(options) {
|
|
|
495
557
|
setOnInput: (handler) => repl.setOnInput(handler),
|
|
496
558
|
missingCredential,
|
|
497
559
|
};
|
|
560
|
+
// Session-scoped subagents render their lifecycle and output through the
|
|
561
|
+
// active renderer (the TUI swaps it in after this runtime is built).
|
|
562
|
+
repl.session.setProcessEventSink((event) => {
|
|
563
|
+
const label = `⚙ ${event.name}`;
|
|
564
|
+
const sink = runtime.renderer;
|
|
565
|
+
if (event.type === 'output' && event.content) {
|
|
566
|
+
sink.writeLine(`${label} ${event.content.trimEnd()}`);
|
|
567
|
+
}
|
|
568
|
+
else if (event.type === 'answer') {
|
|
569
|
+
sink.writeLine(`${label} › ${String(event.answer ?? 'ok')}`);
|
|
570
|
+
}
|
|
571
|
+
else if (event.type === 'exited' && event.state !== 'done') {
|
|
572
|
+
const reason = event.reason && event.reason !== 'normal' ? ` (${String(event.reason)})` : '';
|
|
573
|
+
sink.writeLine(`${label} ${event.state}${reason}`);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
return runtime;
|
|
498
577
|
}
|
|
499
578
|
/** Package version, read once from package.json for the banner/`--version`. */
|
|
500
579
|
export const PACKAGE_VERSION = (() => {
|
|
@@ -546,13 +625,28 @@ async function printBanner(runtime, loaded) {
|
|
|
546
625
|
if (harnessFresh) {
|
|
547
626
|
lines.push('', ` ${bold('Two modes.')} Plain English authors the harness; ${accent('/goal')} runs DML directly.`, '', ` ${accent('<plain English>')} default. Ask for anything: add a new skill, add a tool,`, ` change existing logic, run or test code in the living`, ` module, or just ask a question about the harness. An authoring`, ` agent writes the DML, tests it with ${dim('execute')}, and commits it.`, ` ${accent('/goal <dml>')} run DML/Prolog directly against the current harness.`, '', ` ${bold('Example')}`, ` add a research(Question, Answer) skill that answers with task/2`, ` run research on "why is the sky blue?"`, ` A = "…"`, '', ` ...or run DML directly (${accent('/goal')}):`, ` ${accent('/goal')} research("why is the sky blue?", A).`, ` A = "…"`, '', ` When you're ready, export the harness and re-use it whenever you need:`, ` /export research.dml save a portable harness`, ` ${dim('$')} deepclause repl -l research.dml load it back any time`, ` ${dim('$')} deepclause repl --session last resume the last session`);
|
|
548
627
|
}
|
|
549
|
-
lines.push('', ` ${accent('/goal')} ${dim('<dml>')} run a DML goal, e.g. ${dim('/goal member(X, [1,2,3])')}`, ` ${accent('<plain English>')} author the harness`, '', ` ${accent('/help')} ${accent('/listing')} ${accent('/viz')} ${accent('/export')} ${accent('/freeze')} ${accent('/model')} ${accent('/halt')}`);
|
|
628
|
+
lines.push('', ` ${accent('/goal')} ${dim('<dml>')} run a DML goal, e.g. ${dim('/goal member(X, [1,2,3])')}`, ` ${accent('<plain English>')} author the harness`, ` ${accent('/spawn')} ${dim('<goal>')} start a session-scoped subagent`, ` ${accent('/ps')} ${dim('[--watch|--logs|--code <pid>]')} list subagents / inspect a snapshot`, ` ${accent('/send')} ${accent('/call')} ${accent('/stop')} message, call, or stop a subagent`, '', ` ${accent('/help')} ${accent('/listing')} ${accent('/viz')} ${accent('/export')} ${accent('/freeze')} ${accent('/model')} ${accent('/halt')}`);
|
|
550
629
|
if (runtime.historyEnabled) {
|
|
551
630
|
lines.push(` ${dim('resume later:')} deepclause repl --session ${runtime.sessionId}`);
|
|
552
631
|
}
|
|
553
632
|
lines.push('');
|
|
554
633
|
runtime.renderer.writeLine(lines.join('\n'));
|
|
555
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Replay a resumed session's prior events so the transcript continues instead of
|
|
637
|
+
* starting blank. Bounded, and skipped when the session was not resumed or
|
|
638
|
+
* tracing was off.
|
|
639
|
+
*/
|
|
640
|
+
async function renderResumedSession(runtime) {
|
|
641
|
+
const events = await runtime.repl.resumedTranscript().catch(() => []);
|
|
642
|
+
if (events.length === 0)
|
|
643
|
+
return;
|
|
644
|
+
runtime.renderer.writeLine('');
|
|
645
|
+
runtime.renderer.writeLine(`── resumed session ${runtime.sessionId} ──`);
|
|
646
|
+
for (const event of events)
|
|
647
|
+
runtime.renderer.render(event);
|
|
648
|
+
runtime.renderer.writeLine('');
|
|
649
|
+
}
|
|
556
650
|
/** Dispatch one classified input. Returns false when the REPL should exit. */
|
|
557
651
|
export async function dispatchReplInput(runtime, input, options = {}) {
|
|
558
652
|
const { repl, renderer } = runtime;
|
|
@@ -561,6 +655,53 @@ export async function dispatchReplInput(runtime, input, options = {}) {
|
|
|
561
655
|
case 'empty':
|
|
562
656
|
case 'comment':
|
|
563
657
|
return true;
|
|
658
|
+
case 'spawn': {
|
|
659
|
+
try {
|
|
660
|
+
const pid = await repl.session.spawnProcess(input.goal, input.name ? { name: input.name } : {});
|
|
661
|
+
print(`spawned ${formatProcessTable(repl.session.processes().filter((p) => p.pid.id === pid.id))}`);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
print(`ERROR: ${error.message}`);
|
|
665
|
+
}
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
case 'ps': {
|
|
669
|
+
if (input.code !== undefined) {
|
|
670
|
+
for (const line of formatProcessCode(repl.session, input.code).split('\n'))
|
|
671
|
+
print(line);
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
for (const line of formatProcessTable(repl.session.processes(), repl.session.currentHarnessVersion()).split('\n'))
|
|
675
|
+
print(line);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
case 'send': {
|
|
679
|
+
try {
|
|
680
|
+
repl.session.sendProcess(input.target, input.message);
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
print(`ERROR: ${error.message}`);
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
case 'call': {
|
|
688
|
+
try {
|
|
689
|
+
const reply = await repl.session.callProcess(input.target, input.message, 60_000);
|
|
690
|
+
print(`${input.target} › ${reply}`);
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
print(`ERROR: ${error.message}`);
|
|
694
|
+
}
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
case 'stop': {
|
|
698
|
+
await repl.session.stopProcess(input.target);
|
|
699
|
+
print(`stopped ${input.target}`);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
case 'login':
|
|
703
|
+
print("sign in from the terminal: deepclause login (or /login in the TUI)");
|
|
704
|
+
return true;
|
|
564
705
|
case 'unknown-command':
|
|
565
706
|
print(`unknown command: /${input.name} — try /help.`);
|
|
566
707
|
return true;
|
|
@@ -739,6 +880,11 @@ function helpText() {
|
|
|
739
880
|
' question. An agent writes, tests, and commits the DML.',
|
|
740
881
|
'',
|
|
741
882
|
' Commands',
|
|
883
|
+
' /spawn <goal> start a session-scoped subagent (alias /serve)',
|
|
884
|
+
' /ps [--watch|--logs|--code <pid>] list subagents; --watch pins a panel, --logs follows output, --code shows a process harness snapshot',
|
|
885
|
+
' /send <name> <msg> send a DML term to a subagent (alias /cast)',
|
|
886
|
+
' /call <name> <msg> request/reply with a service subagent',
|
|
887
|
+
' /stop <name> stop a subagent',
|
|
742
888
|
' /listing [filter] inspect the harness (all | Name/Arity | tools | scripts)',
|
|
743
889
|
' /viz [entry] [--spec] diagram of a skill, drawn in the terminal',
|
|
744
890
|
' (--graphics=auto|kitty|iterm2|ascii|off)',
|
|
@@ -750,6 +896,7 @@ function helpText() {
|
|
|
750
896
|
' /compact [notes] compact the authoring memory',
|
|
751
897
|
' /export [file] save the harness (use .zip to bundle linked files)',
|
|
752
898
|
' /model [provider/id] show / set the model',
|
|
899
|
+
' /login sign in to a provider (from the terminal or the TUI)',
|
|
753
900
|
' /freeze /unfreeze read-only lock',
|
|
754
901
|
' /params show session parameters',
|
|
755
902
|
' /trace /notrace toggle verbose logging',
|
|
@@ -782,18 +929,15 @@ async function executeLoad(runtime, files) {
|
|
|
782
929
|
}
|
|
783
930
|
/** Non-interactive mode: read classified inputs line by line until EOF. */
|
|
784
931
|
/**
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
932
|
+
* Ctrl-C exits, in every mode: this is the plain/readline path, where there is no
|
|
933
|
+
* raw key to distinguish Escape from an editing sequence, so the TUI's
|
|
934
|
+
* "Esc interrupts, Ctrl-C exits" cannot be mirrored exactly. A running turn is
|
|
935
|
+
* aborted first, so nothing keeps writing after the UI is gone.
|
|
788
936
|
*/
|
|
789
|
-
function installSigint(
|
|
937
|
+
function installSigint(holder, rl, onExit) {
|
|
790
938
|
const handler = () => {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
runtime.renderer.writeErrorLine('(cancelling current turn… press Ctrl-C again to exit)');
|
|
794
|
-
}
|
|
795
|
-
else if (onExit) {
|
|
796
|
-
// Save the harness before exiting (Ctrl-C while idle).
|
|
939
|
+
holder.turn?.abort(new Error('cancelled by user'));
|
|
940
|
+
if (onExit) {
|
|
797
941
|
void onExit().finally(() => process.exit(130));
|
|
798
942
|
}
|
|
799
943
|
else {
|
|
@@ -811,7 +955,7 @@ async function runNonInteractive(runtime, options, onExit) {
|
|
|
811
955
|
let exitCode = 0;
|
|
812
956
|
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
813
957
|
const holder = { turn: null };
|
|
814
|
-
const removeSigint = installSigint(
|
|
958
|
+
const removeSigint = installSigint(holder, rl, onExit);
|
|
815
959
|
try {
|
|
816
960
|
for await (const line of rl) {
|
|
817
961
|
const input = classifyReplInput(line);
|
|
@@ -881,7 +1025,7 @@ async function runInteractive(runtime, options, onExit) {
|
|
|
881
1025
|
return answer === null ? undefined : answer;
|
|
882
1026
|
});
|
|
883
1027
|
const holder = { turn: null };
|
|
884
|
-
const removeSigint = installSigint(
|
|
1028
|
+
const removeSigint = installSigint(holder, rl, onExit);
|
|
885
1029
|
try {
|
|
886
1030
|
while (true) {
|
|
887
1031
|
const line = await ask(promptPrefix);
|
|
@@ -976,11 +1120,36 @@ export async function runInteractiveGoal(repl, renderer, goal, options, ask, sig
|
|
|
976
1120
|
* input prompts never fight the main prompt for stdin.
|
|
977
1121
|
*/
|
|
978
1122
|
export async function runTui(runtime, options, onExit, loaded = []) {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
|
|
1123
|
+
initTuiTheme();
|
|
1124
|
+
// Sized, because pi-tui falls back to a hard 24 rows when the stream reports no
|
|
1125
|
+
// size, which leaves the bottom of a taller terminal untouched.
|
|
1126
|
+
const terminal = new SizedTerminal();
|
|
1127
|
+
// Fullscreen: an application-owned viewport (ScrollView) with the editor and
|
|
1128
|
+
// status bar pinned in a dock. In this mode pi-tui hands scrolling, mouse wheel,
|
|
1129
|
+
// text selection and search to the viewport, and reassigns layout on resize —
|
|
1130
|
+
// which is what makes the transcript scroll inside the app and the input box
|
|
1131
|
+
// behave when the terminal changes size.
|
|
1132
|
+
const tui = new TuiAltScreen(terminal, undefined, undefined, {
|
|
1133
|
+
mouse: true,
|
|
1134
|
+
wheelScrollLines: 'auto',
|
|
1135
|
+
copyOnSelect: true,
|
|
1136
|
+
});
|
|
1137
|
+
const document = new TranscriptDocument();
|
|
1138
|
+
const transcript = document;
|
|
1139
|
+
// The TUI renders components rather than lines: tool calls become panes and
|
|
1140
|
+
// streamed text becomes markdown. There is no external sink to wire.
|
|
1141
|
+
const working = new WorkingLine(tui);
|
|
1142
|
+
const renderer = new TuiRenderer({
|
|
1143
|
+
document,
|
|
1144
|
+
onWorking: (label) => {
|
|
1145
|
+
working.set(label);
|
|
1146
|
+
tui.requestRender();
|
|
1147
|
+
},
|
|
1148
|
+
});
|
|
1149
|
+
runtime.renderer = renderer;
|
|
1150
|
+
const runState = { running: false, steerable: false, bufferable: false };
|
|
1151
|
+
/** Slash commands typed during a turn/command, run when it finishes. */
|
|
1152
|
+
const pendingCommands = [];
|
|
984
1153
|
/** Lines typed during a goal query, consumed by the solution pager. */
|
|
985
1154
|
const bufferedInput = [];
|
|
986
1155
|
const statusBar = new StatusBarComponent({
|
|
@@ -991,14 +1160,25 @@ export async function runTui(runtime, options, onExit, loaded = []) {
|
|
|
991
1160
|
backend: runtime.backend,
|
|
992
1161
|
isFrozen: () => runtime.repl.isFrozen(),
|
|
993
1162
|
isRunning: () => runState.running,
|
|
1163
|
+
processLabel: () => {
|
|
1164
|
+
const active = runtime.repl.session
|
|
1165
|
+
.processes()
|
|
1166
|
+
.filter((process) => ['running', 'runnable', 'waiting', 'input'].includes(process.state));
|
|
1167
|
+
if (active.length === 0)
|
|
1168
|
+
return undefined;
|
|
1169
|
+
const attention = active.some((process) => process.state === 'input');
|
|
1170
|
+
return `⚙ ${active.length} svc${attention ? ' !' : ''}`;
|
|
1171
|
+
},
|
|
994
1172
|
});
|
|
995
1173
|
transcript.onChange = () => tui.requestRender();
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1174
|
+
renderer.setImageSink((lines) => document.appendImageLines(lines));
|
|
1175
|
+
renderer.setWidth(terminal.columns);
|
|
1176
|
+
// pi-tui re-renders on resize but does not invalidate the component tree, and a
|
|
1177
|
+
// component that caches by width (a tool pane, markdown) would draw stale lines.
|
|
1000
1178
|
const onResize = () => {
|
|
1001
|
-
|
|
1179
|
+
terminal.refreshSize();
|
|
1180
|
+
renderer.setWidth(terminal.columns);
|
|
1181
|
+
tui.invalidate();
|
|
1002
1182
|
tui.requestRender();
|
|
1003
1183
|
};
|
|
1004
1184
|
process.stdout.on('resize', onResize);
|
|
@@ -1027,7 +1207,9 @@ export async function runTui(runtime, options, onExit, loaded = []) {
|
|
|
1027
1207
|
}
|
|
1028
1208
|
else if (activePrompt === runtime.promptPrefix) {
|
|
1029
1209
|
// A user turn: separate it from the previous output and from the
|
|
1030
|
-
// response that follows; render it as a pi-style blue block.
|
|
1210
|
+
// response that follows; render it as a pi-style blue block. The previous
|
|
1211
|
+
// turn's text block is closed first, so the new response starts below this.
|
|
1212
|
+
runtime.renderer.endTextBlock?.();
|
|
1031
1213
|
if (transcript.getLines().length > 0)
|
|
1032
1214
|
transcript.append('');
|
|
1033
1215
|
transcript.append(text, 'user');
|
|
@@ -1037,15 +1219,58 @@ export async function runTui(runtime, options, onExit, loaded = []) {
|
|
|
1037
1219
|
}
|
|
1038
1220
|
else if (text.trim()) {
|
|
1039
1221
|
// No pending ask means a turn is running.
|
|
1040
|
-
|
|
1222
|
+
const trimmed = text.trim();
|
|
1223
|
+
if (isSlashCommand(trimmed)) {
|
|
1224
|
+
// Slash commands are never steering. UI toggles and read-only
|
|
1225
|
+
// snapshots act now; everything else is queued for when the current
|
|
1226
|
+
// turn/command finishes, so /send, /call, /stop … reach the processes
|
|
1227
|
+
// instead of being injected into the model's conversation.
|
|
1228
|
+
const immediate = classifyReplInput(trimmed);
|
|
1229
|
+
transcript.append(trimmed, 'user');
|
|
1230
|
+
if (immediate.kind === 'ps') {
|
|
1231
|
+
if (immediate.logs) {
|
|
1232
|
+
const on = !(servicesVisible && servicesLogs);
|
|
1233
|
+
servicesLogs = on;
|
|
1234
|
+
servicesVisible = true;
|
|
1235
|
+
}
|
|
1236
|
+
else if (immediate.watch) {
|
|
1237
|
+
servicesVisible = !servicesVisible;
|
|
1238
|
+
}
|
|
1239
|
+
else if (immediate.code !== undefined) {
|
|
1240
|
+
for (const line of formatProcessCode(runtime.repl.session, immediate.code).split('\n')) {
|
|
1241
|
+
renderer.writeLine(line);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
else {
|
|
1245
|
+
for (const line of formatProcessTable(runtime.repl.session.processes(), runtime.repl.session.currentHarnessVersion()).split('\n')) {
|
|
1246
|
+
renderer.writeLine(line);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
tui.requestRender();
|
|
1250
|
+
}
|
|
1251
|
+
else if (immediate.kind === 'harness') {
|
|
1252
|
+
void handleHarnessCommand(immediate.width);
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
pendingCommands.push(trimmed);
|
|
1256
|
+
transcript.append(' (queued — runs when the current turn finishes)');
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
else if (runState.steerable) {
|
|
1260
|
+
runtime.renderer.endTextBlock?.();
|
|
1041
1261
|
transcript.append(text, 'steer');
|
|
1042
1262
|
runtime.repl.steer(text);
|
|
1043
1263
|
}
|
|
1044
|
-
else {
|
|
1264
|
+
else if (runState.bufferable) {
|
|
1045
1265
|
// A goal query is running: queue the line for the solution pager.
|
|
1046
1266
|
bufferedInput.push(text);
|
|
1047
1267
|
transcript.append(text, 'steer');
|
|
1048
1268
|
}
|
|
1269
|
+
else {
|
|
1270
|
+
// A command (e.g. /call) is running and owns the prompt; typing does
|
|
1271
|
+
// not steer it. Say so rather than mislabeling the line as steering.
|
|
1272
|
+
transcript.append(' (a command is running — wait, or press Ctrl-C)');
|
|
1273
|
+
}
|
|
1049
1274
|
}
|
|
1050
1275
|
if (editor) {
|
|
1051
1276
|
editor.setPrompt(null);
|
|
@@ -1059,6 +1284,7 @@ export async function runTui(runtime, options, onExit, loaded = []) {
|
|
|
1059
1284
|
};
|
|
1060
1285
|
const createEditor = () => {
|
|
1061
1286
|
editor = new PromptEditor(tui, editorTheme());
|
|
1287
|
+
editor.setAutocompleteProvider(createReplAutocomplete(runtime.workspace));
|
|
1062
1288
|
editor.onSubmit = handleSubmit;
|
|
1063
1289
|
editorContainer.clear();
|
|
1064
1290
|
editorContainer.addChild(editor);
|
|
@@ -1097,55 +1323,434 @@ export async function runTui(runtime, options, onExit, loaded = []) {
|
|
|
1097
1323
|
const answer = await ask(`${prompt}${suffix}`, true);
|
|
1098
1324
|
return answer === null ? undefined : answer;
|
|
1099
1325
|
});
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1326
|
+
/** Interrupt the running turn, if any. Returns whether there was one. */
|
|
1327
|
+
const interruptTurn = () => {
|
|
1328
|
+
if (!holder.turn)
|
|
1329
|
+
return false;
|
|
1330
|
+
holder.turn.abort(new Error('cancelled by user'));
|
|
1331
|
+
// Close the streamed block and clear the dock spinner so an interrupted turn
|
|
1332
|
+
// leaves no live line behind.
|
|
1333
|
+
runtime.renderer.endTextBlock?.();
|
|
1334
|
+
working.set(null);
|
|
1335
|
+
transcript.append('(interrupted — Ctrl-C exits)');
|
|
1336
|
+
tui.requestRender();
|
|
1337
|
+
return true;
|
|
1338
|
+
};
|
|
1339
|
+
/** Leave the REPL, aborting anything still running so nothing outlives the UI. */
|
|
1340
|
+
const exitRepl = () => {
|
|
1341
|
+
holder.turn?.abort(new Error('cancelled by user'));
|
|
1107
1342
|
void (onExit ? onExit() : Promise.resolve()).finally(() => {
|
|
1108
1343
|
tui.stop();
|
|
1109
1344
|
process.exit(130);
|
|
1110
1345
|
});
|
|
1111
1346
|
};
|
|
1112
|
-
// Ctrl-C
|
|
1113
|
-
//
|
|
1347
|
+
// Esc interrupts, Ctrl-C exits, Ctrl-D (on an empty editor) exits. Escape is
|
|
1348
|
+
// left alone while a completion list or an overlay owns it, so it keeps its
|
|
1349
|
+
// usual meaning there. Use pi's key matcher so kitty/meta encodings are
|
|
1350
|
+
// recognized, not just raw bytes.
|
|
1114
1351
|
tui.addInputListener((data) => {
|
|
1115
1352
|
if (matchesKey(data, 'ctrl+c')) {
|
|
1116
|
-
|
|
1353
|
+
exitRepl();
|
|
1117
1354
|
return { consume: true };
|
|
1118
1355
|
}
|
|
1119
1356
|
if (matchesKey(data, 'ctrl+d') && !holder.turn && (editor?.getText().length ?? 0) === 0) {
|
|
1120
|
-
|
|
1357
|
+
exitRepl();
|
|
1121
1358
|
return { consume: true };
|
|
1122
1359
|
}
|
|
1360
|
+
if (matchesKey(data, 'escape')
|
|
1361
|
+
&& !tui.hasOverlay()
|
|
1362
|
+
&& !(editor?.isShowingAutocomplete() ?? false)) {
|
|
1363
|
+
if (interruptTurn())
|
|
1364
|
+
return { consume: true };
|
|
1365
|
+
return undefined;
|
|
1366
|
+
}
|
|
1367
|
+
// ctrl+o expands or collapses the most recent tool call, the way pi reveals
|
|
1368
|
+
// full tool output on demand.
|
|
1369
|
+
if (matchesKey(data, 'ctrl+o')) {
|
|
1370
|
+
if (renderer.toggleLastPane())
|
|
1371
|
+
return { consume: true };
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}
|
|
1123
1374
|
return undefined;
|
|
1124
1375
|
});
|
|
1125
|
-
// Fallback for terminals/raw-mode setups that deliver SIGINT instead of \x03
|
|
1126
|
-
|
|
1376
|
+
// Fallback for terminals/raw-mode setups that deliver SIGINT instead of \x03:
|
|
1377
|
+
// the terminal's own Ctrl-C, which now exits.
|
|
1378
|
+
process.on('SIGINT', exitRepl);
|
|
1127
1379
|
createEditor();
|
|
1128
1380
|
for (const line of runtime.inputHistory)
|
|
1129
1381
|
editor?.addToHistory(line);
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1382
|
+
/**
|
|
1383
|
+
* Show a chooser over the transcript and resolve when it closes.
|
|
1384
|
+
*
|
|
1385
|
+
* The overlay owns input while it is up (pi-tui routes keys to the focused
|
|
1386
|
+
* overlay), so the editor does not see the keys the list is using.
|
|
1387
|
+
*/
|
|
1388
|
+
const choose = (build) => {
|
|
1389
|
+
return new Promise((resolve) => {
|
|
1390
|
+
let handle;
|
|
1391
|
+
const done = (picked) => {
|
|
1392
|
+
handle?.hide();
|
|
1393
|
+
if (editor)
|
|
1394
|
+
tui.setFocus(editor);
|
|
1395
|
+
tui.requestRender();
|
|
1396
|
+
resolve(picked);
|
|
1397
|
+
};
|
|
1398
|
+
const picker = build(done);
|
|
1399
|
+
handle = tui.showOverlay(picker, {
|
|
1400
|
+
anchor: 'center',
|
|
1401
|
+
width: '70%',
|
|
1402
|
+
maxHeight: picker.contentHeight + 2,
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
};
|
|
1406
|
+
const pickReasoningMode = async () => {
|
|
1407
|
+
const modes = ['off', 'summary', 'full'];
|
|
1408
|
+
const picked = await choose((done) => new ChoicePicker({
|
|
1409
|
+
title: 'Reasoning',
|
|
1410
|
+
hint: 'How much of the model\'s thinking to show in the transcript',
|
|
1411
|
+
current: runtime.reasoningMode,
|
|
1412
|
+
items: [
|
|
1413
|
+
{ value: 'off', label: 'off', description: 'hide reasoning entirely' },
|
|
1414
|
+
{ value: 'summary', label: 'summary', description: 'a bounded preview' },
|
|
1415
|
+
{ value: 'full', label: 'full', description: 'everything the model emits' },
|
|
1416
|
+
].filter((item) => modes.includes(item.value)),
|
|
1417
|
+
onSelect: (item) => done(item.value),
|
|
1418
|
+
onCancel: () => done(),
|
|
1419
|
+
}));
|
|
1420
|
+
if (!picked)
|
|
1421
|
+
return;
|
|
1422
|
+
const mode = picked;
|
|
1423
|
+
runtime.reasoningMode = mode;
|
|
1424
|
+
runtime.renderer.setReasoningMode?.(mode);
|
|
1425
|
+
await persistSetting({ repl: { showReasoning: mode } }, 'reasoning display');
|
|
1426
|
+
renderer.writeLine(`reasoning display: ${mode}`);
|
|
1427
|
+
};
|
|
1428
|
+
/**
|
|
1429
|
+
* The model's reasoning level (its thinking effort), as opposed to how much of
|
|
1430
|
+
* that reasoning is displayed — `/show-reasoning` is the other one.
|
|
1431
|
+
*/
|
|
1432
|
+
/**
|
|
1433
|
+
* Persist a setting, but never let that failure stop it applying.
|
|
1434
|
+
*
|
|
1435
|
+
* `updateConfig` requires an existing config file (`deepclause init`), and a
|
|
1436
|
+
* picker that threw before applying the change left the selection doing nothing at
|
|
1437
|
+
* all — which read as "I cannot adjust the reasoning".
|
|
1438
|
+
*/
|
|
1439
|
+
const persistSetting = async (patch, label) => {
|
|
1440
|
+
try {
|
|
1441
|
+
await updateConfig(runtime.workspace, patch);
|
|
1442
|
+
}
|
|
1443
|
+
catch (error) {
|
|
1444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1445
|
+
renderer.writeLine(`${label} applied to this session, but not saved: ${message.split('\n')[0]}`);
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
const pickReasoningLevel = async (level) => {
|
|
1449
|
+
const backend = runtime.llmBackend;
|
|
1450
|
+
const supported = (backend?.supportedThinkingLevels?.() ?? THINKING_LEVELS.slice());
|
|
1451
|
+
const apply = async (chosen) => {
|
|
1452
|
+
backend?.setThinkingLevel?.(chosen);
|
|
1453
|
+
runtime.thinkingLevel = chosen;
|
|
1454
|
+
runtime.setThinkingLevel?.(chosen);
|
|
1455
|
+
await persistSetting({ backend: { kind: 'pi', thinkingLevel: chosen } }, 'reasoning level');
|
|
1456
|
+
renderer.writeLine(`reasoning level: ${chosen}`);
|
|
1457
|
+
};
|
|
1458
|
+
if (level) {
|
|
1459
|
+
if (!supported.includes(level)) {
|
|
1460
|
+
renderer.writeLine(`unknown reasoning level '${level}'; supported: ${supported.join(', ')}`);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
await apply(level);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const chosen = await choose((done) => new ChoicePicker({
|
|
1467
|
+
title: 'Reasoning level',
|
|
1468
|
+
hint: 'How hard the model is asked to think (not how much is shown)',
|
|
1469
|
+
current: runtime.thinkingLevel,
|
|
1470
|
+
items: supported.map((value) => ({
|
|
1471
|
+
value,
|
|
1472
|
+
label: value,
|
|
1473
|
+
description: value === 'off' ? 'no reasoning requested' : undefined,
|
|
1474
|
+
})),
|
|
1475
|
+
onSelect: (item) => done(item.value),
|
|
1476
|
+
onCancel: () => done(),
|
|
1477
|
+
}));
|
|
1478
|
+
if (chosen)
|
|
1479
|
+
await apply(chosen);
|
|
1480
|
+
};
|
|
1481
|
+
/**
|
|
1482
|
+
* `/harness` toggles the side view; `/harness 40`, `wider` and `narrower` resize it.
|
|
1483
|
+
* The width is a layout basis, so a resize rebuilds the root.
|
|
1484
|
+
*/
|
|
1485
|
+
const handleHarnessCommand = async (width) => {
|
|
1486
|
+
if (!width) {
|
|
1487
|
+
harnessVisible = !harnessVisible;
|
|
1488
|
+
applyLayout();
|
|
1489
|
+
if (harnessVisible)
|
|
1490
|
+
harnessView.refresh();
|
|
1491
|
+
tui.invalidate();
|
|
1492
|
+
tui.requestRender();
|
|
1493
|
+
renderer.writeLine(`harness view ${harnessVisible ? 'on' : 'off'}${harnessVisible ? ` (${harnessWidth} columns)` : ''}`);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const requested = width === 'wider' ? harnessWidth + 8
|
|
1497
|
+
: width === 'narrower' ? harnessWidth - 8
|
|
1498
|
+
: Number(width);
|
|
1499
|
+
if (!Number.isFinite(requested) || requested < 20 || requested > 120) {
|
|
1500
|
+
renderer.writeLine(`harness view width must be 20-120 columns (got '${width}')`);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
harnessWidth = Math.round(requested);
|
|
1504
|
+
if (!harnessVisible)
|
|
1505
|
+
harnessVisible = true;
|
|
1506
|
+
applyLayout();
|
|
1507
|
+
tui.invalidate();
|
|
1508
|
+
tui.requestRender();
|
|
1509
|
+
renderer.writeLine(`harness view ${harnessWidth} columns`);
|
|
1510
|
+
};
|
|
1511
|
+
const pickModel = async () => {
|
|
1512
|
+
const catalog = runtime.modelResolver?.catalog() ?? [];
|
|
1513
|
+
if (catalog.length === 0) {
|
|
1514
|
+
renderer.writeLine(`model: ${runtime.model} (no catalog to choose from; set one in config)`);
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const current = runtime.model.includes(':') ? runtime.model : undefined;
|
|
1518
|
+
const picked = await choose((done) => new ChoicePicker({
|
|
1519
|
+
title: 'Model',
|
|
1520
|
+
hint: 'The default for this workspace (applies to the next session)',
|
|
1521
|
+
current,
|
|
1522
|
+
items: catalog.map((entry) => ({
|
|
1523
|
+
value: entry.model,
|
|
1524
|
+
label: entry.model,
|
|
1525
|
+
description: entry.inputCost !== undefined
|
|
1526
|
+
? `$${entry.inputCost}/$${entry.outputCost ?? '?'} per 1M in/out`
|
|
1527
|
+
: (entry.contextWindow ? `${Math.round(entry.contextWindow / 1000)}k context` : undefined),
|
|
1528
|
+
})),
|
|
1529
|
+
onSelect: (item) => done(item.value),
|
|
1530
|
+
onCancel: () => done(),
|
|
1531
|
+
}));
|
|
1532
|
+
if (!picked)
|
|
1533
|
+
return;
|
|
1534
|
+
await persistSetting({ backend: { kind: 'pi', model: picked } }, 'model');
|
|
1535
|
+
renderer.writeLine(`default model set to ${picked} (takes effect on the next start)`);
|
|
1536
|
+
};
|
|
1537
|
+
/** `/login`: sign in to a provider from inside the TUI. */
|
|
1538
|
+
const runLogin = async () => {
|
|
1539
|
+
let handle;
|
|
1540
|
+
try {
|
|
1541
|
+
handle = await openPiRuntime({});
|
|
1542
|
+
}
|
|
1543
|
+
catch (error) {
|
|
1544
|
+
renderer.writeLine(`login unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const providers = listLoginProviders(handle);
|
|
1548
|
+
if (providers.length === 0) {
|
|
1549
|
+
renderer.writeLine('no login providers available');
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const picked = await choose((done) => new ChoicePicker({
|
|
1553
|
+
title: 'Sign in to a provider',
|
|
1554
|
+
hint: 'Esc to cancel',
|
|
1555
|
+
items: providers.map((provider) => ({
|
|
1556
|
+
value: provider.id,
|
|
1557
|
+
label: `${provider.name} (${provider.id})`,
|
|
1558
|
+
description: provider.configured ? '✓ configured' : provider.authType,
|
|
1559
|
+
})),
|
|
1560
|
+
onSelect: (item) => done(item.value),
|
|
1561
|
+
onCancel: () => done(),
|
|
1562
|
+
}));
|
|
1563
|
+
if (!picked)
|
|
1564
|
+
return;
|
|
1565
|
+
const interaction = {
|
|
1566
|
+
prompt: async (prompt) => {
|
|
1567
|
+
if (prompt.options && prompt.options.length > 0) {
|
|
1568
|
+
const method = await choose((done) => new ChoicePicker({
|
|
1569
|
+
title: prompt.message,
|
|
1570
|
+
items: prompt.options.map((option) => ({
|
|
1571
|
+
value: option.id,
|
|
1572
|
+
label: option.label,
|
|
1573
|
+
description: option.description,
|
|
1574
|
+
})),
|
|
1575
|
+
onSelect: (item) => done(item.value),
|
|
1576
|
+
onCancel: () => done(),
|
|
1577
|
+
}));
|
|
1578
|
+
if (!method)
|
|
1579
|
+
throw new Error('cancelled');
|
|
1580
|
+
return method;
|
|
1581
|
+
}
|
|
1582
|
+
const answer = await ask(`${prompt.message}${prompt.placeholder ? ` (${prompt.placeholder})` : ''} `);
|
|
1583
|
+
if (answer === null)
|
|
1584
|
+
throw new Error('cancelled');
|
|
1585
|
+
return answer;
|
|
1586
|
+
},
|
|
1587
|
+
notify: (event) => {
|
|
1588
|
+
if (event.type === 'auth_url' && event.url)
|
|
1589
|
+
renderer.writeLine(`open this URL to authorize: ${event.url}`);
|
|
1590
|
+
else if (event.type === 'device_code')
|
|
1591
|
+
renderer.writeLine(`visit ${event.verificationUri} and enter code ${event.userCode}`);
|
|
1592
|
+
else if (event.message)
|
|
1593
|
+
renderer.writeLine(event.message);
|
|
1594
|
+
},
|
|
1595
|
+
};
|
|
1596
|
+
try {
|
|
1597
|
+
await loginToProvider(handle, picked, { interaction });
|
|
1598
|
+
await persistSetting({ backend: { kind: 'pi' } }, 'login');
|
|
1599
|
+
renderer.writeLine(`signed in to ${picked} (run /model to pick one of its models)`);
|
|
1600
|
+
}
|
|
1601
|
+
catch (error) {
|
|
1602
|
+
renderer.writeLine(`login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
// The experiment: a live side view of the harness, beside the transcript. It reads
|
|
1606
|
+
// the session module (so uncommitted edits show) and refreshes after anything that
|
|
1607
|
+
// can change it. Hidden on a narrow terminal, where there is no room for a column.
|
|
1608
|
+
const harnessView = new HarnessView(() => runtime.repl.session.harnessView(), {
|
|
1609
|
+
// The view asks; the host fetches (the listing query is async) and hands it back.
|
|
1610
|
+
requestSource: (indicator) => {
|
|
1611
|
+
void runtime.repl.session.listing(indicator).then((text) => {
|
|
1612
|
+
harnessView.setSource(indicator, text.split('\n'));
|
|
1613
|
+
tui.requestRender();
|
|
1614
|
+
});
|
|
1615
|
+
},
|
|
1616
|
+
// Escape with nothing drilled into: hand the keyboard back to the editor.
|
|
1617
|
+
onExit: () => {
|
|
1618
|
+
if (editor)
|
|
1619
|
+
tui.setFocus(editor);
|
|
1620
|
+
tui.requestRender();
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
harnessView.refresh();
|
|
1624
|
+
let harnessVisible = options.harnessView ?? false;
|
|
1625
|
+
let servicesVisible = false;
|
|
1626
|
+
let servicesLogs = false;
|
|
1627
|
+
const servicesPanel = new ServicesPanel({
|
|
1628
|
+
getProcesses: () => runtime.repl.session.processes(),
|
|
1629
|
+
getLog: () => runtime.repl.session.processLog(200),
|
|
1630
|
+
isVisible: () => servicesVisible,
|
|
1631
|
+
mode: () => (servicesLogs ? 'logs' : 'summary'),
|
|
1632
|
+
currentHarnessVersion: () => runtime.repl.session.currentHarnessVersion(),
|
|
1633
|
+
});
|
|
1634
|
+
const refreshHarness = () => {
|
|
1635
|
+
if (!harnessVisible)
|
|
1636
|
+
return;
|
|
1637
|
+
harnessView.refresh();
|
|
1638
|
+
tui.requestRender();
|
|
1639
|
+
};
|
|
1640
|
+
// Poll while the view is open. It is meant to be watched, and a change has to be
|
|
1641
|
+
// visible when it happens, not at the next turn boundary.
|
|
1642
|
+
const harnessTimer = setInterval(refreshHarness, 600);
|
|
1643
|
+
const servicesTimer = setInterval(() => {
|
|
1644
|
+
if (!servicesVisible)
|
|
1645
|
+
return;
|
|
1646
|
+
const active = runtime.repl.session
|
|
1647
|
+
.processes()
|
|
1648
|
+
.some((process) => ['running', 'runnable', 'waiting', 'input'].includes(process.state));
|
|
1649
|
+
if (active || servicesLogs)
|
|
1650
|
+
tui.requestRender();
|
|
1651
|
+
}, 600);
|
|
1652
|
+
// Transcript grows and scrolls; everything below it stays put.
|
|
1653
|
+
const scroll = new ScrollView(document, {
|
|
1654
|
+
follow: 'end',
|
|
1655
|
+
primary: true,
|
|
1656
|
+
scrollbar: 'auto',
|
|
1657
|
+
scrollbarTrackStyle: scrollbarStyles.track,
|
|
1658
|
+
scrollbarThumbStyle: scrollbarStyles.thumb,
|
|
1659
|
+
});
|
|
1660
|
+
// Margins: a blank row above the transcript and one between it and the editor, so
|
|
1661
|
+
// the first and last line of output are not jammed against the chrome. Fixed
|
|
1662
|
+
// spacers rather than padding inside the scroll view: they do not scroll away and
|
|
1663
|
+
// do not consume scrollback.
|
|
1664
|
+
const dock = new VStack([
|
|
1665
|
+
{ component: working, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
|
|
1666
|
+
{ component: servicesPanel, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
|
|
1667
|
+
{ component: new Spacer(1), basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
|
|
1668
|
+
{ component: editorContainer, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
|
|
1669
|
+
{ component: statusBar, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
|
|
1670
|
+
]);
|
|
1671
|
+
// Rebuilt rather than mutated: a stack entry's basis is read from the entries given
|
|
1672
|
+
// at construction, so resizing means handing the TUI a new root.
|
|
1673
|
+
let harnessWidth = 34;
|
|
1674
|
+
const applyLayout = () => {
|
|
1675
|
+
const columns = new HStack([
|
|
1676
|
+
{ component: scroll, basis: 0, grow: 1, shrink: 1, minSize: 20 },
|
|
1677
|
+
{
|
|
1678
|
+
component: harnessView,
|
|
1679
|
+
basis: harnessWidth,
|
|
1680
|
+
grow: 0,
|
|
1681
|
+
shrink: 0,
|
|
1682
|
+
minSize: 20,
|
|
1683
|
+
visible: (viewport) => harnessVisible && viewport.width >= 96,
|
|
1684
|
+
},
|
|
1685
|
+
], { gap: 1 });
|
|
1686
|
+
tui.setLayoutRoot(new VStack([
|
|
1687
|
+
{ component: new Spacer(1), basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
|
|
1688
|
+
{ component: columns, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
1689
|
+
{ component: dock, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
|
|
1690
|
+
]));
|
|
1691
|
+
};
|
|
1692
|
+
applyLayout();
|
|
1134
1693
|
tui.start();
|
|
1135
1694
|
await printBanner(runtime, loaded);
|
|
1695
|
+
await renderResumedSession(runtime);
|
|
1136
1696
|
tui.requestRender();
|
|
1137
1697
|
try {
|
|
1138
1698
|
while (true) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1699
|
+
// A slash command typed while a turn/command was running: run it now that
|
|
1700
|
+
// the REPL is idle, before asking for fresh input.
|
|
1701
|
+
let line;
|
|
1702
|
+
if (pendingCommands.length > 0) {
|
|
1703
|
+
line = pendingCommands.shift();
|
|
1704
|
+
tui.requestRender();
|
|
1705
|
+
}
|
|
1706
|
+
else {
|
|
1707
|
+
line = await ask(runtime.promptPrefix);
|
|
1708
|
+
if (line === null)
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1142
1711
|
const input = classifyReplInput(line);
|
|
1143
1712
|
if (input.kind === 'halt')
|
|
1144
1713
|
break;
|
|
1714
|
+
// The pickers are overlays over the transcript, so they need the TUI and are
|
|
1715
|
+
// handled here rather than in the command dispatcher the readline path shares.
|
|
1716
|
+
if (input.kind === 'model' && !input.model) {
|
|
1717
|
+
await pickModel();
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (input.kind === 'reasoning') {
|
|
1721
|
+
await pickReasoningLevel(input.level);
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
if (input.kind === 'show-reasoning') {
|
|
1725
|
+
await pickReasoningMode();
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
if (input.kind === 'harness') {
|
|
1729
|
+
await handleHarnessCommand(input.width);
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (input.kind === 'login') {
|
|
1733
|
+
await runLogin();
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (input.kind === 'ps' && (input.watch || input.logs)) {
|
|
1737
|
+
if (input.logs) {
|
|
1738
|
+
// Toggle the follow-the-log view; it is always visible when on.
|
|
1739
|
+
const turningOn = !(servicesVisible && servicesLogs);
|
|
1740
|
+
servicesLogs = turningOn;
|
|
1741
|
+
servicesVisible = true;
|
|
1742
|
+
}
|
|
1743
|
+
else {
|
|
1744
|
+
servicesVisible = !servicesVisible;
|
|
1745
|
+
}
|
|
1746
|
+
tui.requestRender();
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1145
1749
|
const controller = new AbortController();
|
|
1146
1750
|
holder.turn = controller;
|
|
1147
1751
|
runState.running = true;
|
|
1148
1752
|
runState.steerable = input.kind === 'prompt';
|
|
1753
|
+
runState.bufferable = input.kind === 'goal';
|
|
1149
1754
|
bufferedInput.length = 0;
|
|
1150
1755
|
tui.requestRender();
|
|
1151
1756
|
try {
|
|
@@ -1171,8 +1776,11 @@ export async function runTui(runtime, options, onExit, loaded = []) {
|
|
|
1171
1776
|
}
|
|
1172
1777
|
finally {
|
|
1173
1778
|
tui.stop();
|
|
1174
|
-
process.off('SIGINT',
|
|
1779
|
+
process.off('SIGINT', exitRepl);
|
|
1175
1780
|
process.stdout.off('resize', onResize);
|
|
1781
|
+
clearInterval(harnessTimer);
|
|
1782
|
+
clearInterval(servicesTimer);
|
|
1783
|
+
working.stop();
|
|
1176
1784
|
// Restore stdout so the exit footer and any later output are visible.
|
|
1177
1785
|
runtime.renderer.setSink((text) => process.stdout.write(text), (text) => process.stderr.write(text));
|
|
1178
1786
|
}
|
|
@@ -1194,6 +1802,9 @@ export async function startRepl(options) {
|
|
|
1194
1802
|
channel: message.channel,
|
|
1195
1803
|
});
|
|
1196
1804
|
}
|
|
1805
|
+
if (options.execution === 'worker') {
|
|
1806
|
+
runtime.renderer.writeLine('note: subagents run in a worker thread (--execution worker)');
|
|
1807
|
+
}
|
|
1197
1808
|
}
|
|
1198
1809
|
catch (error) {
|
|
1199
1810
|
process.stderr.write(`ERROR: ${error.message}\n`);
|
|
@@ -1239,7 +1850,7 @@ export async function startRepl(options) {
|
|
|
1239
1850
|
if (goals.length > 0) {
|
|
1240
1851
|
let exitCode = 0;
|
|
1241
1852
|
const controller = new AbortController();
|
|
1242
|
-
const removeSigint = installSigint(
|
|
1853
|
+
const removeSigint = installSigint({ turn: controller });
|
|
1243
1854
|
try {
|
|
1244
1855
|
for (const goal of goals) {
|
|
1245
1856
|
if (controller.signal.aborted)
|
|
@@ -1271,6 +1882,7 @@ export async function startRepl(options) {
|
|
|
1271
1882
|
return await runTui(runtime, options, saveHarnessOnExit, options.load ?? []);
|
|
1272
1883
|
}
|
|
1273
1884
|
await printBanner(runtime, options.load ?? []);
|
|
1885
|
+
await renderResumedSession(runtime);
|
|
1274
1886
|
return await runInteractive(runtime, options, saveHarnessOnExit);
|
|
1275
1887
|
}
|
|
1276
1888
|
finally {
|
|
@@ -1330,6 +1942,14 @@ export function registerReplCommand(program) {
|
|
|
1330
1942
|
.option('--no-tools', 'register no capability host tools')
|
|
1331
1943
|
.option('--sandbox', 'run shell tools in AgentVM')
|
|
1332
1944
|
.option('--require-sandbox', 'refuse to run unless the shell is sandboxed (bwrap/AgentVM/sandbox-exec)')
|
|
1945
|
+
.option('--harness-view', 'open the live harness side view (experiment)')
|
|
1946
|
+
.option('--execution <mode>', 'process execution mode: worker (L2, default) or inline (L1)', (value) => {
|
|
1947
|
+
if (value !== 'inline' && value !== 'worker') {
|
|
1948
|
+
throw new Error(`invalid --execution: ${value} (use inline or worker)`);
|
|
1949
|
+
}
|
|
1950
|
+
return value;
|
|
1951
|
+
})
|
|
1952
|
+
.option('--workers <n>', 'worker pool size for --execution worker', parseInt)
|
|
1333
1953
|
.option('--no-ask', 'make ask_user / input/2 fail instead of prompting')
|
|
1334
1954
|
.option('--image-model <provider:id>', 'image model for generation (default: first available)')
|
|
1335
1955
|
.option('--no-open', 'do not hand images to the OS viewer on terminals without inline images')
|
|
@@ -1375,6 +1995,9 @@ export function registerReplCommand(program) {
|
|
|
1375
1995
|
noTools: options.tools === false,
|
|
1376
1996
|
sandbox: options.sandbox,
|
|
1377
1997
|
requireSandbox: options.requireSandbox,
|
|
1998
|
+
harnessView: options.harnessView,
|
|
1999
|
+
execution: options.execution,
|
|
2000
|
+
workers: options.workers,
|
|
1378
2001
|
noAsk: options.ask === false,
|
|
1379
2002
|
imageModel: options.imageModel,
|
|
1380
2003
|
open: options.open,
|