indusagi-coding-agent 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/entry.js +1762 -958
- package/dist/guardrails.js +145 -35
- package/dist/index.js +1810 -1006
- package/dist/review-agent.js +344 -60
- package/dist/review-server.js +346 -62
- package/package.json +12 -3
- package/src/boot/auth-vault.ts +4 -5
- package/src/boot/runners/addon-wiring.ts +111 -3
- package/src/boot/runners/benchmark-integrity.ts +107 -0
- package/src/boot/runners/repl-runner.ts +2 -1
- package/src/boot/runners/session.ts +18 -8
- package/src/boot/server-token.ts +20 -11
- package/src/briefing/context-docs.ts +144 -12
- package/src/channels/oneshot.ts +6 -0
- package/src/conductor/conductor.ts +67 -12
- package/src/conductor/contract.ts +4 -3
- package/src/conductor/transcript-store/store.ts +4 -8
- package/src/console/auth-status.ts +9 -4
- package/src/console/components/AgentsView.ts +2 -2
- package/src/console/components/BackgroundAgents.ts +15 -2
- package/src/console/components/Banner.ts +20 -9
- package/src/console/components/Composer.ts +57 -82
- package/src/console/components/ScrollViewport.ts +46 -0
- package/src/console/components/TerminalConsole.ts +165 -162
- package/src/console/components/WorkingIndicator.ts +6 -5
- package/src/console/components/banner-sweep.ts +5 -1
- package/src/console/components/welcome.ts +3 -3
- package/src/console/contract.ts +10 -5
- package/src/console/index.ts +7 -7
- package/src/console/input/complete.ts +2 -2
- package/src/console/input/dir-reader.ts +3 -1
- package/src/console/input/graphemes.ts +22 -0
- package/src/console/input/keymap.ts +6 -4
- package/src/console/mount.ts +45 -3
- package/src/console/overlays/auth.ts +28 -8
- package/src/console/overlays/boards.ts +13 -40
- package/src/console/overlays/host.ts +10 -0
- package/src/console/overlays/pickers.ts +35 -30
- package/src/console/reducer.ts +12 -5
- package/src/console/slash/builtins.ts +4 -3
- package/src/console/slash/commands/integrations.ts +13 -14
- package/src/console/slash/commands/transcript.ts +3 -8
- package/src/console/slash/commands/workbench.ts +2 -2
- package/src/console/test-support.ts +48 -0
- package/src/integrations/sarvam/config.ts +15 -13
- package/src/kit/external-editor.ts +17 -1
- package/src/kit/run-shell.ts +31 -0
- package/src/launch/invocation/flags.ts +12 -0
- package/src/launch/login.ts +23 -10
- package/src/launch/oauth.ts +3 -0
- package/src/models.ts +78 -2
- package/src/security/private-state.ts +122 -0
- package/src/window-budget/summarize/condense.ts +4 -1
- package/src/workspace/brand.ts +1 -1
- package/src/workspace/locator.ts +2 -2
|
@@ -5,6 +5,7 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import React, { useEffect, useMemo, useState } from "indusagi/react-host";
|
|
7
7
|
import { Box, Text, useInput } from "indusagi/react-host/ink";
|
|
8
|
+
import { useStdin, useStdout } from "ink";
|
|
8
9
|
import { MessageList, StreamingMarkdown, TaskPanel, type InkThemeAdapter } from "indusagi/react-ink";
|
|
9
10
|
import { Fragment, jsx, jsxs } from "indusagi/react-host/jsx-runtime";
|
|
10
11
|
import { VERSION } from "../../index.js";
|
|
@@ -41,7 +42,10 @@ import {
|
|
|
41
42
|
EMPTY_APPROVAL_QUEUE,
|
|
42
43
|
modeReleasesApprovals
|
|
43
44
|
} from "../overlays/approval-queue.js";
|
|
45
|
+
import type { ApprovalChoice } from "../../conductor/index.js";
|
|
44
46
|
import type { ConsoleProps } from "../contract.js";
|
|
47
|
+
import { isThemeScheme } from "../contract.js";
|
|
48
|
+
import { resolveTheme } from "../theme/resolve.js";
|
|
45
49
|
import { OverlayHost } from "../overlays/host.js";
|
|
46
50
|
import { useAuthenticatedProviders } from "../auth-status.js";
|
|
47
51
|
import { gatherStartup } from "../startup.js";
|
|
@@ -52,6 +56,7 @@ import { Banner } from "./Banner.js";
|
|
|
52
56
|
import { Composer } from "./Composer.js";
|
|
53
57
|
import { StatusBar } from "./StatusBar.js";
|
|
54
58
|
import { WorkingIndicator } from "./WorkingIndicator.js";
|
|
59
|
+
import { ScrollViewport, useTerminalSize } from "./ScrollViewport.js";
|
|
55
60
|
|
|
56
61
|
const { useReducer, useRef, useCallback } = React;
|
|
57
62
|
|
|
@@ -220,8 +225,27 @@ function permissionModeStatusText(mode: string): string {
|
|
|
220
225
|
|
|
221
226
|
export function TerminalConsole(props: ConsoleProps) {
|
|
222
227
|
const { conductor, theme, slash, verbose, onExit, initialInput, services } = props;
|
|
223
|
-
const
|
|
224
|
-
const
|
|
228
|
+
const {stdin, internal_eventEmitter} = useStdin();
|
|
229
|
+
const {stdout} = useStdout();
|
|
230
|
+
// Ink 5 labels both DEL (the usual Backspace byte) and CSI 3~ as delete.
|
|
231
|
+
// Preserve the raw chunk before Ink's parser discards that distinction.
|
|
232
|
+
const rawKey = useRef("");
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const capture = (data: string | Buffer) => { rawKey.current = String(data); };
|
|
235
|
+
internal_eventEmitter.prependListener("input", capture);
|
|
236
|
+
return () => { internal_eventEmitter.removeListener("input", capture); };
|
|
237
|
+
}, [internal_eventEmitter]);
|
|
238
|
+
const [state, dispatch] = useReducer(consoleReducer, theme.scheme, (scheme) => {
|
|
239
|
+
let stored: unknown;
|
|
240
|
+
try { stored = services?.settings.get("colourScheme"); } catch { /* use host theme */ }
|
|
241
|
+
const flag = (key: "showReasoning" | "showImages", fallback: boolean) => {
|
|
242
|
+
try { const value = services?.settings.get(key); return typeof value === "boolean" ? value : fallback; }
|
|
243
|
+
catch { return fallback; }
|
|
244
|
+
};
|
|
245
|
+
return initConsoleState({ scheme: typeof stored === "string" && isThemeScheme(stored) ? stored : scheme,
|
|
246
|
+
showReasoning: flag("showReasoning", false), showImages: flag("showImages", true) });
|
|
247
|
+
});
|
|
248
|
+
const adapter = resolveTheme(state.scheme).adapter;
|
|
225
249
|
const chord = useRef(NO_CHORD);
|
|
226
250
|
const chordTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
227
251
|
const paste = useRef(EMPTY_PASTE_VAULT);
|
|
@@ -232,13 +256,20 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
232
256
|
const liveReason = useRef<string | null>(null);
|
|
233
257
|
const rowSeed = useRef(0);
|
|
234
258
|
const ctrlCAt = useRef(0);
|
|
259
|
+
const resumeHandler = useRef<(() => void) | null>(null);
|
|
260
|
+
useEffect(() => () => { if (resumeHandler.current) process.off("SIGCONT", resumeHandler.current); }, []);
|
|
235
261
|
const [highlight, setHighlight] = useState(0);
|
|
236
262
|
const [toolExecutions, setToolExecutions] = useState<Record<string, any>>({});
|
|
237
263
|
const [taskTokens, setTaskTokens] = useState<Record<string, number>>({});
|
|
238
264
|
const [subagentActivity, setSubagentActivity] = useState<Record<string, any[]>>({});
|
|
239
265
|
const [expandTools, setExpandTools] = useState(false);
|
|
240
266
|
const [agentsOpen, setAgentsOpen] = useState(false);
|
|
241
|
-
const
|
|
267
|
+
const { rows: terminalRows, columns: terminalColumns } = useTerminalSize();
|
|
268
|
+
// Ink clears the entire terminal whenever rendered output is as tall as the
|
|
269
|
+
// terminal (`outputHeight >= stdout.rows`). Leave one physical row unused so
|
|
270
|
+
// ordinary input edits use Ink's incremental line updates instead of a
|
|
271
|
+
// full-screen clear, which visibly flickers over Windows Terminal/SSH.
|
|
272
|
+
const renderRows = Math.max(1, terminalRows - 1);
|
|
242
273
|
const [, forceTick] = useState(0);
|
|
243
274
|
const pulse = useCallback(() => forceTick((n) => n + 1), []);
|
|
244
275
|
const dirReader = useMemo(() => createDirReader(process.cwd()), []);
|
|
@@ -260,7 +291,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
260
291
|
}, []);
|
|
261
292
|
const requestApproval = useCallback(
|
|
262
293
|
(toolName: string, input: unknown, opts: { signal?: AbortSignal }) =>
|
|
263
|
-
new Promise((resolve) => {
|
|
294
|
+
new Promise<ApprovalChoice>((resolve) => {
|
|
264
295
|
approvalSeed.current += 1;
|
|
265
296
|
const id = `ap_${approvalSeed.current}`;
|
|
266
297
|
const request = {
|
|
@@ -268,17 +299,19 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
268
299
|
input,
|
|
269
300
|
suggestions: allowAlwaysRuleStrings(toolName, input)
|
|
270
301
|
};
|
|
271
|
-
approvalResolvers.current.set(id, resolve);
|
|
272
302
|
const signal = opts.signal;
|
|
303
|
+
const onAbort = () => settleApproval(id, "deny");
|
|
304
|
+
const finish = (choice: ApprovalChoice) => { signal?.removeEventListener("abort", onAbort); resolve(choice); };
|
|
305
|
+
approvalResolvers.current.set(id, finish);
|
|
273
306
|
if (signal !== void 0) {
|
|
274
307
|
if (signal.aborted) {
|
|
275
308
|
approvalResolvers.current.delete(id);
|
|
276
309
|
resolve("deny");
|
|
277
310
|
return;
|
|
278
311
|
}
|
|
279
|
-
signal.addEventListener("abort",
|
|
312
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
280
313
|
}
|
|
281
|
-
dispatchApproval({ type: "enqueue", entry: { id, request, resolve } });
|
|
314
|
+
dispatchApproval({ type: "enqueue", entry: { id, request, resolve: finish } });
|
|
282
315
|
}),
|
|
283
316
|
[settleApproval]
|
|
284
317
|
);
|
|
@@ -324,10 +357,31 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
324
357
|
const reasonText = useRef("");
|
|
325
358
|
const visibleFrom = useRef(0);
|
|
326
359
|
const [compactNote, setCompactNote] = useState<string[] | null>(null);
|
|
360
|
+
const sessionId = conductor.snapshot().head.sessionId;
|
|
361
|
+
const priorSession = useRef(sessionId);
|
|
362
|
+
const resetView = useCallback(() => {
|
|
363
|
+
liveAnswer.current = null; liveReason.current = null;
|
|
364
|
+
answerText.current = ""; reasonText.current = ""; visibleFrom.current = 0;
|
|
365
|
+
activePaste.current = null; paste.current = EMPTY_PASTE_VAULT; burst.current = "";
|
|
366
|
+
if (burstTimer.current !== null) { clearTimeout(burstTimer.current); burstTimer.current = null; }
|
|
367
|
+
setToolExecutions({}); setTaskTokens({}); setSubagentActivity({}); setCompactNote(null); setAgentsOpen(false);
|
|
368
|
+
denyAllApprovals(); dispatch({type:"view:reset"});
|
|
369
|
+
}, [denyAllApprovals]);
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (priorSession.current !== sessionId) { priorSession.current = sessionId; resetView(); }
|
|
372
|
+
}, [sessionId, resetView]);
|
|
327
373
|
useEffect(() => {
|
|
328
374
|
const off = conductor.subscribe((signal: any) => {
|
|
329
375
|
switch (signal.kind) {
|
|
376
|
+
case "reset": { resetView(); break; }
|
|
377
|
+
case "phase": {
|
|
378
|
+
dispatch({type:"busy:set",busy:signal.phase === "streaming" || signal.phase === "tooling" || signal.phase === "condensing"});
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
330
381
|
case "prompt": {
|
|
382
|
+
liveAnswer.current = null; liveReason.current = null;
|
|
383
|
+
answerText.current = ""; reasonText.current = "";
|
|
384
|
+
dispatch({type:"busy:set",busy:true});
|
|
331
385
|
break;
|
|
332
386
|
}
|
|
333
387
|
case "text": {
|
|
@@ -335,10 +389,8 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
335
389
|
const id = freshRowId(rowSeed.current++);
|
|
336
390
|
liveAnswer.current = id;
|
|
337
391
|
answerText.current = signal.delta;
|
|
338
|
-
dispatch({ type: "rows:append", row: { id, kind: "answer", text: answerText.current } });
|
|
339
392
|
} else {
|
|
340
393
|
answerText.current += signal.delta;
|
|
341
|
-
dispatch({ type: "rows:patch", id: liveAnswer.current, text: answerText.current });
|
|
342
394
|
}
|
|
343
395
|
break;
|
|
344
396
|
}
|
|
@@ -347,19 +399,14 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
347
399
|
const id = freshRowId(rowSeed.current++);
|
|
348
400
|
liveReason.current = id;
|
|
349
401
|
reasonText.current = signal.delta;
|
|
350
|
-
dispatch({ type: "rows:append", row: { id, kind: "reason", text: reasonText.current } });
|
|
351
402
|
} else {
|
|
352
403
|
reasonText.current += signal.delta;
|
|
353
|
-
dispatch({ type: "rows:patch", id: liveReason.current, text: reasonText.current });
|
|
354
404
|
}
|
|
355
405
|
break;
|
|
356
406
|
}
|
|
357
407
|
case "tool_start": {
|
|
358
408
|
liveAnswer.current = null;
|
|
359
409
|
liveReason.current = null;
|
|
360
|
-
const id = `tool-${signal.id}`;
|
|
361
|
-
const row = { id, kind: "toolRun", text: signal.name, runId: signal.id };
|
|
362
|
-
dispatch({ type: "rows:append", row });
|
|
363
410
|
setToolExecutions((current) => ({
|
|
364
411
|
...current,
|
|
365
412
|
[signal.id]: {
|
|
@@ -404,11 +451,6 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
404
451
|
break;
|
|
405
452
|
}
|
|
406
453
|
case "tool_end": {
|
|
407
|
-
dispatch({
|
|
408
|
-
type: "rows:patch",
|
|
409
|
-
id: `tool-${signal.id}`,
|
|
410
|
-
text: signal.ok ? "done" : "failed"
|
|
411
|
-
});
|
|
412
454
|
setToolExecutions((current) => {
|
|
413
455
|
const prior = current[signal.id];
|
|
414
456
|
if (prior === void 0) return current;
|
|
@@ -433,7 +475,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
433
475
|
if (signal.manual) {
|
|
434
476
|
const carried: string[] = [];
|
|
435
477
|
for (const message of conductor.messages()) {
|
|
436
|
-
const content = message.content;
|
|
478
|
+
const content = "content" in message ? message.content : undefined;
|
|
437
479
|
if (typeof content === "string" && content.startsWith(RESTORED_FILE_PREFIX)) {
|
|
438
480
|
const firstLine = content.slice(RESTORED_FILE_PREFIX.length).split("\n", 1)[0];
|
|
439
481
|
if (firstLine) carried.push(firstLine.trim());
|
|
@@ -445,7 +487,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
445
487
|
liveReason.current = null;
|
|
446
488
|
answerText.current = "";
|
|
447
489
|
reasonText.current = "";
|
|
448
|
-
dispatch({ type: "
|
|
490
|
+
dispatch({ type: "blocks:clear" });
|
|
449
491
|
}
|
|
450
492
|
dispatch({
|
|
451
493
|
type: "status:set",
|
|
@@ -474,6 +516,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
474
516
|
break;
|
|
475
517
|
}
|
|
476
518
|
case "idle": {
|
|
519
|
+
liveAnswer.current = null; liveReason.current = null;
|
|
477
520
|
dispatch({ type: "busy:set", busy: false });
|
|
478
521
|
break;
|
|
479
522
|
}
|
|
@@ -483,7 +526,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
483
526
|
pulse();
|
|
484
527
|
});
|
|
485
528
|
return off;
|
|
486
|
-
}, [conductor, pulse]);
|
|
529
|
+
}, [conductor, pulse, resetView]);
|
|
487
530
|
const completion = useMemo(
|
|
488
531
|
() => completeAt(state.buffer, state.caret, slash, dirReader),
|
|
489
532
|
[state.buffer, state.caret, slash, dirReader]
|
|
@@ -494,8 +537,6 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
494
537
|
}, [suggestions.length, highlight]);
|
|
495
538
|
const runTurn = useCallback(
|
|
496
539
|
async (text: string) => {
|
|
497
|
-
const id = freshRowId(rowSeed.current++);
|
|
498
|
-
dispatch({ type: "rows:append", row: { id, kind: "prompt", text } });
|
|
499
540
|
dispatch({ type: "busy:set", busy: true });
|
|
500
541
|
if (conductor.snapshot().phase === "condensing") {
|
|
501
542
|
dispatch({
|
|
@@ -505,8 +546,8 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
505
546
|
}
|
|
506
547
|
setCompactNote(null);
|
|
507
548
|
activePaste.current = null;
|
|
508
|
-
|
|
509
|
-
|
|
549
|
+
// The conductor may only enqueue this input. Live buffers belong to the
|
|
550
|
+
// running turn and are reset by actual turn lifecycle signals.
|
|
510
551
|
try {
|
|
511
552
|
await conductor.submit(text);
|
|
512
553
|
} catch (err) {
|
|
@@ -525,9 +566,10 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
525
566
|
const text = hasMarkers(raw, paste.current) ? expandMarkers(raw, paste.current) : raw;
|
|
526
567
|
const trimmed = text.trim();
|
|
527
568
|
dispatch({ type: "buffer:set", text: "" });
|
|
528
|
-
paste.current =
|
|
569
|
+
paste.current = { bodies: new Map(), minted: paste.current.minted };
|
|
570
|
+
activePaste.current = null;
|
|
529
571
|
if (trimmed.length === 0) return;
|
|
530
|
-
dispatch({ type: "history:push", text
|
|
572
|
+
dispatch({ type: "history:push", text });
|
|
531
573
|
if (trimmed.startsWith("!")) {
|
|
532
574
|
const excludeFromContext = trimmed.startsWith("!!");
|
|
533
575
|
const command = trimmed.slice(excludeFromContext ? 2 : 1).trim();
|
|
@@ -536,6 +578,13 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
536
578
|
void conductor
|
|
537
579
|
.executeBash(command, { excludeFromContext })
|
|
538
580
|
.then((outcome: any) => {
|
|
581
|
+
const output = String(outcome.output ?? "");
|
|
582
|
+
const fence = "`".repeat(Math.max(3, ...Array.from(output.matchAll(/`+/g), m => m[0].length + 1)));
|
|
583
|
+
const recorded = conductor.messages().some((message: any) => message.role === "user" && message.content === `$ ${command}\n${output}`.trimEnd());
|
|
584
|
+
if (!recorded || excludeFromContext) dispatch({ type: "block:append", block: {
|
|
585
|
+
id: freshRowId(rowSeed.current++), kind: "changelog", title: `$ ${command}`,
|
|
586
|
+
markdown: `${fence}text\n${output || "(no output)"}\n${fence}`, timestamp: Date.now()
|
|
587
|
+
} });
|
|
539
588
|
dispatch({
|
|
540
589
|
type: "status:set",
|
|
541
590
|
status: {
|
|
@@ -554,9 +603,9 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
554
603
|
}
|
|
555
604
|
const resolution = resolveSlash(trimmed, slash);
|
|
556
605
|
if (resolution.kind === "match") {
|
|
557
|
-
|
|
606
|
+
void Promise.resolve().then(() => resolution.command.run({
|
|
558
607
|
args: resolution.args,
|
|
559
|
-
conductor,
|
|
608
|
+
conductor, services,
|
|
560
609
|
dispatch,
|
|
561
610
|
openModal: (kind, payload) => dispatch({ type: "modal:open", kind, payload }),
|
|
562
611
|
closeModal: () => dispatch({ type: "modal:close" }),
|
|
@@ -564,10 +613,9 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
564
613
|
setStatus: (status) => dispatch({ type: "status:set", status }),
|
|
565
614
|
setBuffer: (value) => dispatch({ type: "buffer:set", text: value }),
|
|
566
615
|
appendBlock: (block) => dispatch({ type: "block:append", block })
|
|
567
|
-
})
|
|
568
|
-
void Promise.resolve(outcome).then((settled: any) => {
|
|
616
|
+
})).then((settled: any) => {
|
|
569
617
|
if (settled.kind === "prompt") void runTurn(settled.text);
|
|
570
|
-
});
|
|
618
|
+
}).catch((error) => dispatch({type:"status:set",status:{kind:"error",text:error instanceof Error ? error.message : String(error)}}));
|
|
571
619
|
return;
|
|
572
620
|
}
|
|
573
621
|
if (resolution.kind === "miss") {
|
|
@@ -579,7 +627,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
579
627
|
}
|
|
580
628
|
void runTurn(text);
|
|
581
629
|
},
|
|
582
|
-
[slash, conductor, onExit, runTurn]
|
|
630
|
+
[slash, conductor, onExit, runTurn, services]
|
|
583
631
|
);
|
|
584
632
|
const flushBurst = useCallback(() => {
|
|
585
633
|
if (burstTimer.current !== null) {
|
|
@@ -639,13 +687,6 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
639
687
|
},
|
|
640
688
|
[]
|
|
641
689
|
);
|
|
642
|
-
useEffect(() => {
|
|
643
|
-
const onResize = () => setTerminalRows(process.stdout.rows ?? FALLBACK_TERMINAL_ROWS);
|
|
644
|
-
process.stdout.on("resize", onResize);
|
|
645
|
-
return () => {
|
|
646
|
-
process.stdout.off("resize", onResize);
|
|
647
|
-
};
|
|
648
|
-
}, []);
|
|
649
690
|
const titleName = conductor.sessionName();
|
|
650
691
|
useEffect(() => {
|
|
651
692
|
writeTerminalTitle(titleName && titleName.length > 0 ? titleName : "indus console");
|
|
@@ -666,7 +707,8 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
666
707
|
(rawInput, key) => {
|
|
667
708
|
const input = rawInput.replace(BRACKETED_PASTE, "");
|
|
668
709
|
if (input.length === 0 && input !== rawInput) return;
|
|
669
|
-
const
|
|
710
|
+
const isBackspaceByte = rawKey.current === "\x7f" || rawKey.current === "\b";
|
|
711
|
+
const intent = readKey(input, {...key, backspace:key.backspace || isBackspaceByte, delete:key.delete && !isBackspaceByte});
|
|
670
712
|
if (intent.verb === "text:type") {
|
|
671
713
|
burst.current += intent.text ?? "";
|
|
672
714
|
armChord(NO_CHORD);
|
|
@@ -803,7 +845,8 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
803
845
|
return;
|
|
804
846
|
}
|
|
805
847
|
case "flow:cycleModel":
|
|
806
|
-
conductor.cycleModel?.("");
|
|
848
|
+
conductor.cycleModel?.("", intent.direction ?? 1);
|
|
849
|
+
pulse();
|
|
807
850
|
return;
|
|
808
851
|
case "model:cycleThinking": {
|
|
809
852
|
const level = conductor.cycleThinkingLevel();
|
|
@@ -859,7 +902,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
859
902
|
return;
|
|
860
903
|
}
|
|
861
904
|
case "input:externalEditor": {
|
|
862
|
-
const edited = openInExternalEditor(state.buffer);
|
|
905
|
+
const edited = openInExternalEditor(expandMarkers(state.buffer, paste.current), {terminal:{stdin,stdout}});
|
|
863
906
|
if (edited !== null) {
|
|
864
907
|
dispatch({ type: "buffer:set", text: edited });
|
|
865
908
|
}
|
|
@@ -873,9 +916,25 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
873
916
|
case "view:expandTools":
|
|
874
917
|
setExpandTools((current) => !current);
|
|
875
918
|
return;
|
|
876
|
-
case "flow:suspend":
|
|
877
|
-
|
|
919
|
+
case "flow:suspend": {
|
|
920
|
+
if (process.platform === "win32") {
|
|
921
|
+
dispatch({type:"status:set",status:{kind:"warning",text:"Process suspension is not supported on Windows."}});
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const wasRaw = stdin.isRaw === true;
|
|
925
|
+
const resume = () => {
|
|
926
|
+
process.off("SIGCONT", resume); resumeHandler.current = null;
|
|
927
|
+
stdin.setRawMode?.(wasRaw);
|
|
928
|
+
if (stdout.isTTY) stdout.write("\x1b[?25l");
|
|
929
|
+
dispatch({type:"status:set",status:{kind:"info",text:"Resumed console."}});
|
|
930
|
+
};
|
|
931
|
+
resumeHandler.current = resume;
|
|
932
|
+
process.once("SIGCONT", resume);
|
|
933
|
+
stdin.setRawMode?.(false);
|
|
934
|
+
if (stdout.isTTY) stdout.write("\x1b[?25h");
|
|
935
|
+
try { process.kill(process.pid, "SIGTSTP"); } catch { resume(); }
|
|
878
936
|
return;
|
|
937
|
+
}
|
|
879
938
|
case "none":
|
|
880
939
|
default:
|
|
881
940
|
return;
|
|
@@ -883,6 +942,14 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
883
942
|
},
|
|
884
943
|
{ isActive: state.modal.kind === "none" && !agentsOpen }
|
|
885
944
|
);
|
|
945
|
+
// Global interrupt ownership also applies while a dialog or drill-in has focus.
|
|
946
|
+
useInput((input, key) => {
|
|
947
|
+
if (!key.ctrl || input !== "c") return;
|
|
948
|
+
if (state.busy || conductor.isBusy?.() || conductor.snapshot().phase === "condensing") {
|
|
949
|
+
denyAllApprovals(); conductor.abort();
|
|
950
|
+
}
|
|
951
|
+
dispatch({type:"modal:close"}); setAgentsOpen(false);
|
|
952
|
+
}, {isActive: state.modal.kind !== "none" || agentsOpen});
|
|
886
953
|
useInput(
|
|
887
954
|
(_rawInput, key) => {
|
|
888
955
|
if (key.tab && key.shift) cyclePermission();
|
|
@@ -926,7 +993,7 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
926
993
|
const taskPanelRows = showTaskPanel ? 2 + Math.min(pendingMessages.length, 8) : 0;
|
|
927
994
|
const workingIndicatorRows = state.busy ? 2 : 0;
|
|
928
995
|
const maxItems = transcriptHeight(
|
|
929
|
-
|
|
996
|
+
renderRows,
|
|
930
997
|
modalOpen,
|
|
931
998
|
agentPanelRows(agentRuns) + streamingAnswerRows + taskPanelRows + workingIndicatorRows
|
|
932
999
|
);
|
|
@@ -963,7 +1030,8 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
963
1030
|
live = false;
|
|
964
1031
|
};
|
|
965
1032
|
}, [services]);
|
|
966
|
-
const
|
|
1033
|
+
const authRefreshKey = useMemo(() => ({status:state.status,modal:state.modal.kind}), [state.status,state.modal.kind]);
|
|
1034
|
+
const authedProviders = useAuthenticatedProviders(services, authRefreshKey);
|
|
967
1035
|
const boundModelId = conductor.snapshot().modelId;
|
|
968
1036
|
const boundProvider = boundModelId.includes("/") ? boundModelId.split("/")[0] : boundModelId;
|
|
969
1037
|
const modelAuthed = authedProviders === null || authedProviders.has(boundProvider);
|
|
@@ -972,119 +1040,54 @@ export function TerminalConsole(props: ConsoleProps) {
|
|
|
972
1040
|
modelAuthed || !snapshot.model
|
|
973
1041
|
? snapshot
|
|
974
1042
|
: { ...snapshot, model: { ...snapshot.model, provider: "no model", id: "run /login" } };
|
|
975
|
-
return
|
|
976
|
-
flexDirection: "column",
|
|
977
|
-
children:
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
children: adapter.muted("No files re-attached; the summary digest carries the history.")
|
|
1025
|
-
})
|
|
1026
|
-
]
|
|
1027
|
-
})
|
|
1028
|
-
: null,
|
|
1029
|
-
jsx(MessageList, {
|
|
1030
|
-
messages: liveMessages,
|
|
1031
|
-
theme: adapter,
|
|
1032
|
-
showThinking: state.showReasoning,
|
|
1033
|
-
showImages: state.showImages,
|
|
1034
|
-
expandToolOutputs: expandTools,
|
|
1035
|
-
maxItems,
|
|
1036
|
-
displayBlocks: [...state.blocks]
|
|
1037
|
-
}),
|
|
1038
|
-
liveAnswer.current !== null && answerText.current
|
|
1039
|
-
? jsx(Box, {
|
|
1040
|
-
flexDirection: "column",
|
|
1041
|
-
marginBottom: 1,
|
|
1042
|
-
children: jsx(StreamingMarkdown, { theme: adapter, children: answerText.current })
|
|
1043
|
-
})
|
|
1044
|
-
: null,
|
|
1045
|
-
showTaskPanel
|
|
1046
|
-
? jsx(TaskPanel, {
|
|
1047
|
-
theme: adapter,
|
|
1048
|
-
snapshot,
|
|
1049
|
-
toolExecutions: SHOW_TOOL_ACTIVITY ? toolExecutions : {},
|
|
1050
|
-
expandToolOutputs: expandTools,
|
|
1051
|
-
pendingMessages
|
|
1052
|
-
})
|
|
1053
|
-
: null,
|
|
1054
|
-
jsx(WorkingIndicator, {
|
|
1055
|
-
theme: adapter,
|
|
1056
|
-
busy: state.busy || conductor.snapshot().phase === "condensing",
|
|
1057
|
-
phase: conductor.snapshot().phase
|
|
1058
|
-
}),
|
|
1059
|
-
jsx(Composer, {
|
|
1060
|
-
theme: adapter,
|
|
1061
|
-
buffer: state.buffer,
|
|
1062
|
-
caret: state.caret,
|
|
1063
|
-
busy: state.busy,
|
|
1064
|
-
suggestions,
|
|
1065
|
-
highlight
|
|
1066
|
-
}),
|
|
1067
|
-
jsx(StatusBar, {
|
|
1068
|
-
theme: adapter,
|
|
1069
|
-
snapshot: statusBarSnapshot,
|
|
1070
|
-
branch,
|
|
1071
|
-
providerCount,
|
|
1072
|
-
status: state.status,
|
|
1073
|
-
permissionMode: conductor.permissionMode()
|
|
1074
|
-
}),
|
|
1075
|
-
jsx(BackgroundAgents, { theme: adapter, runs: agentRuns, tokens: taskTokens })
|
|
1076
|
-
]
|
|
1077
|
-
}),
|
|
1078
|
-
jsx(OverlayHost, {
|
|
1079
|
-
modal: state.modal,
|
|
1080
|
-
services,
|
|
1081
|
-
theme: adapter,
|
|
1082
|
-
dispatch,
|
|
1083
|
-
closeModal: () => dispatch({ type: "modal:close" })
|
|
1084
|
-
})
|
|
1085
|
-
]
|
|
1043
|
+
return jsx(Box, {
|
|
1044
|
+
flexDirection: "column", width: terminalColumns, height: renderRows, overflow: "hidden",
|
|
1045
|
+
children: modalOpen
|
|
1046
|
+
? jsx(ScrollViewport, { follow: false, children: jsx(OverlayHost, {
|
|
1047
|
+
modal: state.modal, services, theme: adapter, dispatch,
|
|
1048
|
+
closeModal: () => dispatch({ type: "modal:close" })
|
|
1049
|
+
}) }, state.modal.kind)
|
|
1050
|
+
: agentsOpen
|
|
1051
|
+
? jsx(ScrollViewport, { follow: false, children: jsx(AgentsView, {
|
|
1052
|
+
theme: adapter, runs: agentRuns, activity: subagentActivity, tokens: taskTokens,
|
|
1053
|
+
onExit: () => setAgentsOpen(false)
|
|
1054
|
+
}) }, "agents")
|
|
1055
|
+
: jsxs(Fragment, { children: [
|
|
1056
|
+
terminalRows >= 18 ? jsx(Box, { flexShrink: 0, flexDirection: "column", children: jsx(Banner, {
|
|
1057
|
+
theme: adapter, modelId: bannerModelId, workspace: process.cwd(), version: VERSION,
|
|
1058
|
+
verbose, quiet: quietStartup, compact: liveMessages.length > 0 || terminalRows < 32,
|
|
1059
|
+
started: liveMessages.length > 0, branch, name: welcomeName, sweep,
|
|
1060
|
+
startup: startupMap, variantSeed: welcomeSeed
|
|
1061
|
+
}) }) : null,
|
|
1062
|
+
jsx(ScrollViewport, { children: jsxs(Fragment, { children: [
|
|
1063
|
+
compactNote !== null ? jsx(Text, { children: adapter.muted(
|
|
1064
|
+
`Context condensed.${compactNote.length ? " Files carried: " + compactNote.join(", ") : ""}`
|
|
1065
|
+
) }) : null,
|
|
1066
|
+
jsx(MessageList, {
|
|
1067
|
+
messages: liveMessages, theme: adapter, showThinking: state.showReasoning,
|
|
1068
|
+
showImages: state.showImages, expandToolOutputs: expandTools,
|
|
1069
|
+
maxItems: liveMessages.length + state.blocks.length, displayBlocks: [...state.blocks]
|
|
1070
|
+
}),
|
|
1071
|
+
state.showReasoning && liveReason.current !== null && reasonText.current ? jsx(Box, {
|
|
1072
|
+
flexDirection: "column", marginBottom: 1,
|
|
1073
|
+
children: jsx(StreamingMarkdown, {theme: adapter, dim: true, children: reasonText.current})
|
|
1074
|
+
}) : null,
|
|
1075
|
+
liveAnswer.current !== null && answerText.current ? jsx(Box, {
|
|
1076
|
+
flexDirection: "column", marginBottom: 1,
|
|
1077
|
+
children: jsx(StreamingMarkdown, {theme: adapter, children: answerText.current})
|
|
1078
|
+
}) : null,
|
|
1079
|
+
showTaskPanel ? jsx(TaskPanel, {theme: adapter, snapshot,
|
|
1080
|
+
toolExecutions: SHOW_TOOL_ACTIVITY ? toolExecutions : {},
|
|
1081
|
+
expandToolOutputs: expandTools, pendingMessages}) : null,
|
|
1082
|
+
jsx(BackgroundAgents, {theme: adapter, runs: agentRuns, tokens: taskTokens})
|
|
1083
|
+
] }) }, snapshot.sessionId),
|
|
1084
|
+
jsx(Box, {flexDirection: "column", flexShrink: 0, children: jsxs(Fragment, {children: [
|
|
1085
|
+
jsx(WorkingIndicator, {theme: adapter, busy: state.busy || conductor.snapshot().phase === "condensing", phase: conductor.snapshot().phase, reducedMotion: services?.settings.get("reducedMotion") ?? false}),
|
|
1086
|
+
jsx(Composer, {theme: adapter, buffer: state.buffer, caret: state.caret,
|
|
1087
|
+
busy: state.busy, suggestions, highlight, paddingX: services?.settings.get("editorPaddingX") ?? 1}),
|
|
1088
|
+
terminalRows >= 12 ? jsx(StatusBar, {theme: adapter, snapshot: statusBarSnapshot, branch,
|
|
1089
|
+
providerCount, status: state.status, permissionMode: conductor.permissionMode()}) : null
|
|
1090
|
+
]})})
|
|
1091
|
+
]})
|
|
1086
1092
|
});
|
|
1087
1093
|
}
|
|
1088
|
-
|
|
1089
|
-
// Silence unused warnings for cross-module imports that may not be used directly here.
|
|
1090
|
-
void NO_INTENT;
|
|
@@ -65,9 +65,10 @@ export interface WorkingIndicatorProps {
|
|
|
65
65
|
readonly theme: InkThemeAdapter;
|
|
66
66
|
readonly busy: boolean;
|
|
67
67
|
readonly phase?: string;
|
|
68
|
+
readonly reducedMotion?: boolean;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export function WorkingIndicator({ theme, busy, phase }: WorkingIndicatorProps) {
|
|
71
|
+
export function WorkingIndicator({ theme, busy, phase, reducedMotion = false }: WorkingIndicatorProps) {
|
|
71
72
|
const [frame, setFrame] = useState(0);
|
|
72
73
|
const startRef = useRef<number | null>(null);
|
|
73
74
|
const wordOffsetRef = useRef(0);
|
|
@@ -80,14 +81,14 @@ export function WorkingIndicator({ theme, busy, phase }: WorkingIndicatorProps)
|
|
|
80
81
|
startRef.current = Date.now();
|
|
81
82
|
wordOffsetRef.current = Math.floor(Math.random() * WORKING_WORDS.length);
|
|
82
83
|
}
|
|
83
|
-
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), FRAME_MS);
|
|
84
|
+
const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), reducedMotion ? 1000 : FRAME_MS);
|
|
84
85
|
return () => clearInterval(id);
|
|
85
|
-
}, [busy]);
|
|
86
|
+
}, [busy, reducedMotion]);
|
|
86
87
|
if (!busy) return null;
|
|
87
88
|
const elapsedMs = startRef.current !== null ? Date.now() - startRef.current : 0;
|
|
88
89
|
const elapsedSec = Math.floor(elapsedMs / 1000);
|
|
89
|
-
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
90
|
-
const label = phaseLabel(phase) ?? pickWord(wordOffsetRef.current + Math.floor(elapsedMs / WORD_MS));
|
|
90
|
+
const spinner = reducedMotion ? "•" : SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
91
|
+
const label = phaseLabel(phase) ?? (reducedMotion ? "Working" : pickWord(wordOffsetRef.current + Math.floor(elapsedMs / WORD_MS)));
|
|
91
92
|
return jsxs(Box, {
|
|
92
93
|
marginTop: 1,
|
|
93
94
|
children: [
|
|
@@ -20,5 +20,9 @@ export function welcomeLine(name?: string): string {
|
|
|
20
20
|
*/
|
|
21
21
|
export function rowGradient(rowIndex: number, rowCount: number, primaryHex: string, secondaryHex: string): string {
|
|
22
22
|
if (rowCount <= 1) return primaryHex;
|
|
23
|
-
|
|
23
|
+
const t = Math.max(0, Math.min(1, rowIndex / (rowCount - 1)));
|
|
24
|
+
const a = Number.parseInt(primaryHex.replace("#", ""), 16);
|
|
25
|
+
const b = Number.parseInt(secondaryHex.replace("#", ""), 16);
|
|
26
|
+
const channel = (shift: number) => Math.round(((a >> shift) & 255) * (1 - t) + ((b >> shift) & 255) * t).toString(16).padStart(2, "0");
|
|
27
|
+
return `#${channel(16)}${channel(8)}${channel(0)}`;
|
|
24
28
|
}
|
|
@@ -36,12 +36,12 @@ export function mascotRows(face: MascotFace): readonly (readonly MascotSegment[]
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export const TIPS: readonly string[] = [
|
|
39
|
-
"Run /
|
|
39
|
+
"Run /help to explore commands and configured skill helpers",
|
|
40
40
|
'Ask in plain English — try "fix the failing tests" or "explain src/agent.ts"',
|
|
41
|
-
"Shift+Tab cycles
|
|
41
|
+
"Shift+Tab cycles: default → accept-edits → plan → bypass",
|
|
42
42
|
"Type @ to attach a file, or paste an image straight into the prompt",
|
|
43
43
|
"Run /status to check your model, account, and connectivity",
|
|
44
|
-
"Use /
|
|
44
|
+
"Use /help to discover specialized commands for the task at hand"
|
|
45
45
|
];
|
|
46
46
|
|
|
47
47
|
export const WHATS_NEW: readonly string[] = [
|