pum-agent 0.2.13-beta.1 → 0.2.15-beta.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 +1 -1
- package/package.json +1 -1
- package/src/app.tsx +162 -18
- package/src/check-mode.ts +10 -3
- package/src/check-policy.ts +3 -1
- package/src/commands.ts +5 -1
- package/src/help-popup.tsx +2 -2
- package/src/main.tsx +39 -0
- package/src/output-minimal.ts +135 -0
- package/src/processes-popup.tsx +270 -0
- package/src/replay.ts +93 -14
- package/src/sandbox/index.ts +77 -1
- package/src/sandbox-policy.ts +3 -1
- package/src/settings-popup.tsx +2 -0
- package/src/settings.ts +20 -0
- package/src/shells/lifecycle.ts +134 -0
- package/src/shells/manager.ts +494 -0
- package/src/shells/process.ts +108 -0
- package/src/shells/tools.ts +361 -0
- package/src/shells/types.ts +195 -0
- package/src/shutdown.ts +7 -2
- package/src/status-bar.tsx +14 -1
- package/src/subagents/fork-session.ts +80 -0
- package/src/subagents/manager.ts +253 -40
- package/src/subagents/spawn-preview-popup.tsx +14 -7
- package/src/subagents/types.ts +21 -0
- package/src/tool-groups.ts +17 -1
- package/src/tool-line.ts +10 -1
- package/src/tool-preview.ts +133 -0
- package/src/transcript-output.ts +30 -0
- package/src/transcript.tsx +202 -16
- package/src/triggers/popup.tsx +3 -1
- package/src/writing-style.ts +9 -6
package/README.md
CHANGED
|
@@ -325,7 +325,7 @@ Set `PUM_DIR` to override the complete PUM data directory.
|
|
|
325
325
|
| `auth.json` | Provider credentials and custom-provider keys |
|
|
326
326
|
| `models.json` | Custom endpoints and model metadata; submitted keys are not stored here |
|
|
327
327
|
| `settings.json` | Model and thinking level managed by pi |
|
|
328
|
-
| `pum.json` | Theme, animation, search, writing, explanation, Check mode, sandbox, and subagent settings |
|
|
328
|
+
| `pum.json` | Theme, animation, transcript output, search, writing, explanation, Check mode, sandbox, and subagent settings |
|
|
329
329
|
| `theme.json` | Optional semantic color overrides |
|
|
330
330
|
| `history.json` | Prompt history by working directory |
|
|
331
331
|
| `prompt-stash.json` | Stashed prompts by working directory |
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
CHECK_MODE_PROFILES,
|
|
27
27
|
checkPathsForProject,
|
|
28
|
+
cycleOutputMode,
|
|
28
29
|
MAX_ACTIVE_SUBAGENTS,
|
|
29
30
|
MIN_ACTIVE_SUBAGENTS,
|
|
30
31
|
SANDBOX_MODES,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
type Role,
|
|
49
50
|
} from "./transcript";
|
|
50
51
|
import { bashOutput, bashResultDisplay, editCounts, toolArg, type ToolCall } from "./tool-line";
|
|
52
|
+
import { toolPreviewFromResult, toolPreviewFromStart } from "./tool-preview";
|
|
51
53
|
import { readBranch, watchBranch } from "./git-branch";
|
|
52
54
|
import { HelpPopup, maxHelpScrollOffset } from "./help-popup";
|
|
53
55
|
import { appendHistory, loadHistory, removeHistory } from "./history";
|
|
@@ -119,10 +121,17 @@ import {
|
|
|
119
121
|
moveTriggerSelection,
|
|
120
122
|
sortTriggers,
|
|
121
123
|
triggerActionForKey,
|
|
122
|
-
TriggersPopup,
|
|
123
124
|
type TriggerAction,
|
|
124
125
|
type TriggerManagerLike,
|
|
125
126
|
} from "./triggers/popup";
|
|
127
|
+
import {
|
|
128
|
+
moveProcessSelection,
|
|
129
|
+
processTabForKey,
|
|
130
|
+
ProcessesPopup,
|
|
131
|
+
sortShells,
|
|
132
|
+
type ProcessTab,
|
|
133
|
+
type ShellManagerLike,
|
|
134
|
+
} from "./processes-popup";
|
|
126
135
|
import type { TerminalTitleController } from "./terminal-title";
|
|
127
136
|
import { readClipboardText } from "./text-paste";
|
|
128
137
|
import { copyTextToClipboard } from "./clipboard";
|
|
@@ -137,6 +146,10 @@ import {
|
|
|
137
146
|
} from "./news";
|
|
138
147
|
import { statsFromEntries, type SessionStatsManager } from "./session-stats";
|
|
139
148
|
import { maxStatsScrollOffset, StatsPopup } from "./stats-popup";
|
|
149
|
+
import {
|
|
150
|
+
projectTranscriptLines,
|
|
151
|
+
transcriptOutputMode,
|
|
152
|
+
} from "./transcript-output";
|
|
140
153
|
|
|
141
154
|
type Stream = { kind: "assistant" | "thinking"; text: string } | null;
|
|
142
155
|
type Transcript = { lines: Line[]; stream: Stream; pending: PendingLine[] };
|
|
@@ -405,6 +418,7 @@ export function App({
|
|
|
405
418
|
copyNewsAnswerText = copyTextToClipboard,
|
|
406
419
|
onExit = () => process.exit(0),
|
|
407
420
|
triggerManager,
|
|
421
|
+
shellManager,
|
|
408
422
|
messageCacheController,
|
|
409
423
|
terminalTitle,
|
|
410
424
|
startupWarnings = [],
|
|
@@ -434,6 +448,7 @@ export function App({
|
|
|
434
448
|
copyNewsAnswerText?: typeof copyTextToClipboard;
|
|
435
449
|
onExit?: () => void | Promise<void>;
|
|
436
450
|
triggerManager?: TriggerManagerLike;
|
|
451
|
+
shellManager?: ShellManagerLike;
|
|
437
452
|
messageCacheController?: MessageCacheController;
|
|
438
453
|
terminalTitle?: TerminalTitleController;
|
|
439
454
|
/** Visible process-local warnings. These lines never enter pi session context. */
|
|
@@ -528,8 +543,12 @@ export function App({
|
|
|
528
543
|
const [modelSearchFocused, setModelSearchFocused] = useState(false);
|
|
529
544
|
const [, setAgentRevision] = useState(0);
|
|
530
545
|
const [triggersOpen, setTriggersOpen] = useState(false);
|
|
546
|
+
const [processTab, setProcessTab] = useState<ProcessTab>("triggers");
|
|
531
547
|
const [triggerCursor, setTriggerCursor] = useState(0);
|
|
548
|
+
const [shellCursor, setShellCursor] = useState(0);
|
|
532
549
|
const [, setTriggerRevision] = useState(0);
|
|
550
|
+
const [shellRevision, setShellRevision] = useState(0);
|
|
551
|
+
const [shellTails, setShellTails] = useState<Record<string, string>>({});
|
|
533
552
|
|
|
534
553
|
const [newsOpen, setNewsOpen] = useState(false);
|
|
535
554
|
const newsOpenRef = useRef(false);
|
|
@@ -553,6 +572,11 @@ export function App({
|
|
|
553
572
|
const questionnaire = questionnaireManager?.current();
|
|
554
573
|
const spawnPreview = spawnPreviewManager?.current();
|
|
555
574
|
const visibleTx = activeAgent?.transcript ?? tx;
|
|
575
|
+
const outputMode = transcriptOutputMode(settings);
|
|
576
|
+
const visibleLines = useMemo(
|
|
577
|
+
() => projectTranscriptLines(visibleTx.lines, outputMode),
|
|
578
|
+
[visibleTx.lines, outputMode],
|
|
579
|
+
);
|
|
556
580
|
const visibleBusy = activeAgent
|
|
557
581
|
? activeAgent.status === "starting" || activeAgent.status === "running"
|
|
558
582
|
: busy;
|
|
@@ -563,6 +587,10 @@ export function App({
|
|
|
563
587
|
const visibleUsage = activeAgent?.usage ?? usage;
|
|
564
588
|
const agentTreeRows = buildAgentTree(agents);
|
|
565
589
|
const triggers = sortTriggers(triggerManager?.getTriggers() ?? []);
|
|
590
|
+
const shells = sortShells(shellManager?.list() ?? []);
|
|
591
|
+
const runningShellCount = shells.filter(
|
|
592
|
+
(shell) => shell.state === "starting" || shell.state === "running",
|
|
593
|
+
).length;
|
|
566
594
|
const statsSnapshot = useMemo(() => statsManager?.snapshot() ?? statsFromEntries(
|
|
567
595
|
(session.sessionManager as any).getEntries?.() ?? session.sessionManager.buildContextEntries(),
|
|
568
596
|
`${session.agent.state.model.provider}/${session.agent.state.model.id}`,
|
|
@@ -591,12 +619,14 @@ export function App({
|
|
|
591
619
|
const spawnPreviewInputRef = useRef<TextareaRenderable>(null);
|
|
592
620
|
const settingsOpenRef = useRef(settingsOpen);
|
|
593
621
|
const triggersOpenRef = useRef(false);
|
|
622
|
+
const processTabRef = useRef<ProcessTab>("triggers");
|
|
594
623
|
const settingsPageRef = useRef(page);
|
|
595
624
|
const settingsSearchFocusedRef = useRef(settingsSearchFocused);
|
|
596
625
|
const focusInputAfterSwitch = useRef(false);
|
|
597
626
|
const activeAgentIdRef = useRef<string | null>(null);
|
|
598
627
|
const agentSelectorCursorRef = useRef(0);
|
|
599
628
|
const triggerCursorRef = useRef(0);
|
|
629
|
+
const shellCursorRef = useRef(0);
|
|
600
630
|
const commandCursorRef = useRef(0);
|
|
601
631
|
const stashRef = useRef(stash);
|
|
602
632
|
const stashOpenRef = useRef(false);
|
|
@@ -678,11 +708,32 @@ export function App({
|
|
|
678
708
|
setStatsOpen(false);
|
|
679
709
|
statsOpenRef.current = false;
|
|
680
710
|
setStashMode(false);
|
|
711
|
+
processTabRef.current = "triggers";
|
|
712
|
+
setProcessTab("triggers");
|
|
681
713
|
const nextCursor = Math.min(triggerCursorRef.current, Math.max(0, triggers.length - 1));
|
|
682
714
|
triggerCursorRef.current = nextCursor;
|
|
683
715
|
setTriggerCursor(nextCursor);
|
|
684
716
|
setTriggerPopup(true);
|
|
685
717
|
};
|
|
718
|
+
const openProcesses = () => {
|
|
719
|
+
settingsOpenRef.current = false;
|
|
720
|
+
setSettingsOpen(false);
|
|
721
|
+
setHelpOpen(false);
|
|
722
|
+
setHistoryOpen(false);
|
|
723
|
+
setAgentSelectorOpen(false);
|
|
724
|
+
setNewsOpen(false);
|
|
725
|
+
newsOpenRef.current = false;
|
|
726
|
+
setStatsOpen(false);
|
|
727
|
+
statsOpenRef.current = false;
|
|
728
|
+
setStashMode(false);
|
|
729
|
+
const nextTriggerCursor = Math.min(triggerCursorRef.current, Math.max(0, triggers.length - 1));
|
|
730
|
+
const nextShellCursor = Math.min(shellCursorRef.current, Math.max(0, shells.length - 1));
|
|
731
|
+
triggerCursorRef.current = nextTriggerCursor;
|
|
732
|
+
shellCursorRef.current = nextShellCursor;
|
|
733
|
+
setTriggerCursor(nextTriggerCursor);
|
|
734
|
+
setShellCursor(nextShellCursor);
|
|
735
|
+
setTriggerPopup(true);
|
|
736
|
+
};
|
|
686
737
|
// The event subscription is set up once, so it reads the toggle via a ref.
|
|
687
738
|
const showThinkingRef = useRef(initial.showThinking);
|
|
688
739
|
const startupWarningsRef = useRef([...startupWarnings]);
|
|
@@ -1051,6 +1102,28 @@ export function App({
|
|
|
1051
1102
|
setTriggerCursor(next);
|
|
1052
1103
|
}), [triggerManager]);
|
|
1053
1104
|
|
|
1105
|
+
useEffect(() => shellManager?.subscribe(() => {
|
|
1106
|
+
setShellRevision((revision) => revision + 1);
|
|
1107
|
+
const count = shellManager.list().length;
|
|
1108
|
+
const next = Math.min(shellCursorRef.current, Math.max(0, count - 1));
|
|
1109
|
+
shellCursorRef.current = next;
|
|
1110
|
+
setShellCursor(next);
|
|
1111
|
+
}), [shellManager]);
|
|
1112
|
+
|
|
1113
|
+
useEffect(() => {
|
|
1114
|
+
if (!shellManager || !triggersOpen || processTab !== "shells") return;
|
|
1115
|
+
const shell = shells[shellCursor];
|
|
1116
|
+
if (!shell || !shell.output.exists) return;
|
|
1117
|
+
let active = true;
|
|
1118
|
+
void shellManager.getOutput(shell.id, { lineLimit: 20 }).then((result) => {
|
|
1119
|
+
if (!active) return;
|
|
1120
|
+
setShellTails((tails) => tails[shell.id] === result.tail
|
|
1121
|
+
? tails
|
|
1122
|
+
: { ...tails, [shell.id]: result.tail });
|
|
1123
|
+
}).catch(() => {});
|
|
1124
|
+
return () => { active = false; };
|
|
1125
|
+
}, [shellManager, triggersOpen, processTab, shellCursor, shellRevision]);
|
|
1126
|
+
|
|
1054
1127
|
useEffect(
|
|
1055
1128
|
() => subagentManager.subscribe((event) => {
|
|
1056
1129
|
if (event.type === "main-line") append(event.line);
|
|
@@ -1184,6 +1257,8 @@ export function App({
|
|
|
1184
1257
|
arg: toolArg(event.toolName, event.args, cwd),
|
|
1185
1258
|
state: "running",
|
|
1186
1259
|
startedAt: Date.now(),
|
|
1260
|
+
input: event.args,
|
|
1261
|
+
preview: toolPreviewFromStart(event.toolName, event.args),
|
|
1187
1262
|
},
|
|
1188
1263
|
});
|
|
1189
1264
|
break;
|
|
@@ -1194,6 +1269,7 @@ export function App({
|
|
|
1194
1269
|
break;
|
|
1195
1270
|
case "tool_execution_end": {
|
|
1196
1271
|
const bashResult = event.toolName === "bash" ? bashResultDisplay(event.result) : {};
|
|
1272
|
+
const preview = toolPreviewFromResult(event.toolName, event.result);
|
|
1197
1273
|
patchTool(event.toolCallId, {
|
|
1198
1274
|
state: isRejectedToolResult(event.result, event.toolCallId)
|
|
1199
1275
|
? "rejected"
|
|
@@ -1209,8 +1285,10 @@ export function App({
|
|
|
1209
1285
|
: event.toolName.startsWith("message_cache_")
|
|
1210
1286
|
? messageCacheDetail(event.result)
|
|
1211
1287
|
: undefined,
|
|
1212
|
-
output: undefined,
|
|
1213
1288
|
exitCode: bashResult.exitCode,
|
|
1289
|
+
result: event.result,
|
|
1290
|
+
isError: event.isError,
|
|
1291
|
+
...(preview ? { preview } : {}),
|
|
1214
1292
|
});
|
|
1215
1293
|
break;
|
|
1216
1294
|
}
|
|
@@ -1436,6 +1514,10 @@ export function App({
|
|
|
1436
1514
|
update({ workingRuleAnimation: next });
|
|
1437
1515
|
};
|
|
1438
1516
|
|
|
1517
|
+
const stepOutputMode = (step: number) => {
|
|
1518
|
+
update({ outputMode: cycleOutputMode(settings.outputMode, step) });
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1439
1521
|
const openLogin = () => {
|
|
1440
1522
|
settingsOpenRef.current = false;
|
|
1441
1523
|
setSettingsOpen(false);
|
|
@@ -1818,10 +1900,11 @@ export function App({
|
|
|
1818
1900
|
const loginCommand = trimmed === "/login";
|
|
1819
1901
|
const checkPathCommand = /^\/check-path(?:\s|$)/.test(trimmed);
|
|
1820
1902
|
const triggersCommand = trimmed === "/triggers";
|
|
1903
|
+
const processesCommand = trimmed === "/processes";
|
|
1821
1904
|
const newsCommand = trimmed === "/news";
|
|
1822
1905
|
const statsCommand = trimmed === "/stats";
|
|
1823
1906
|
const worktreeCommand = /^\/worktree(?:\s+([a-zA-Z0-9_-]+))?$/.exec(trimmed);
|
|
1824
|
-
if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !newsCommand && !statsCommand && !worktreeCommand) return false;
|
|
1907
|
+
if (!compress && !clear && !historyCommand && !loginCommand && !checkPathCommand && !triggersCommand && !processesCommand && !newsCommand && !statsCommand && !worktreeCommand) return false;
|
|
1825
1908
|
editingStashIndex.current = null;
|
|
1826
1909
|
|
|
1827
1910
|
if (historyCommand) {
|
|
@@ -1839,6 +1922,11 @@ export function App({
|
|
|
1839
1922
|
openTriggers();
|
|
1840
1923
|
return true;
|
|
1841
1924
|
}
|
|
1925
|
+
if (processesCommand) {
|
|
1926
|
+
setEditorText("");
|
|
1927
|
+
openProcesses();
|
|
1928
|
+
return true;
|
|
1929
|
+
}
|
|
1842
1930
|
if (newsCommand) {
|
|
1843
1931
|
setEditorText("");
|
|
1844
1932
|
openNews();
|
|
@@ -2109,6 +2197,16 @@ export function App({
|
|
|
2109
2197
|
}));
|
|
2110
2198
|
};
|
|
2111
2199
|
|
|
2200
|
+
const killSelectedShell = () => {
|
|
2201
|
+
const shell = shells[shellCursorRef.current];
|
|
2202
|
+
if (!shell || !shellManager || (shell.state !== "starting" && shell.state !== "running")) return;
|
|
2203
|
+
Promise.resolve(shellManager.terminate(shell.id)).catch((error) => append({
|
|
2204
|
+
kind: "text",
|
|
2205
|
+
role: "error",
|
|
2206
|
+
text: `shell kill failed: ${String(error)}`,
|
|
2207
|
+
}));
|
|
2208
|
+
};
|
|
2209
|
+
|
|
2112
2210
|
const stepCheckMode = (step: number) => {
|
|
2113
2211
|
const index = CHECK_MODE_PROFILES.indexOf(settings.checkMode);
|
|
2114
2212
|
update({ checkMode: CHECK_MODE_PROFILES[(index + step + CHECK_MODE_PROFILES.length) % CHECK_MODE_PROFILES.length]! });
|
|
@@ -2125,6 +2223,7 @@ export function App({
|
|
|
2125
2223
|
providers: { enter: openLogin },
|
|
2126
2224
|
animations: { step: () => update({ animations: !settings.animations }) },
|
|
2127
2225
|
workingRuleAnimation: { step: stepWorkingRuleAnimation },
|
|
2226
|
+
outputMode: { step: stepOutputMode },
|
|
2128
2227
|
webSearch: { step: () => update({ webSearch: !settings.webSearch }) },
|
|
2129
2228
|
writingStyle: { step: stepWritingStyle },
|
|
2130
2229
|
explanationStrength: { step: stepExplanationStrength },
|
|
@@ -2167,6 +2266,7 @@ export function App({
|
|
|
2167
2266
|
providers: "login and custom setup ›",
|
|
2168
2267
|
animations: `‹ ${settings.animations ? "on" : "off"} ›`,
|
|
2169
2268
|
workingRuleAnimation: `‹ ${settings.workingRuleAnimation} ›${settings.workingRuleAnimation === "off" ? "" : animationUnavailable}`,
|
|
2269
|
+
outputMode: `‹ ${settings.outputMode ?? "default"} ›`,
|
|
2170
2270
|
webSearch: `‹ ${settings.webSearch ? "on" : "off"} ›${searchProviders.length ? "" : " (not on provider)"}`,
|
|
2171
2271
|
writingStyle: `‹ ${settings.writingStyle} ›`,
|
|
2172
2272
|
explanationStrength: `‹ ${settings.explanationStrength} ›`,
|
|
@@ -2388,7 +2488,7 @@ export function App({
|
|
|
2388
2488
|
if (key.ctrl && key.name === "t") {
|
|
2389
2489
|
key.stopPropagation();
|
|
2390
2490
|
if (triggersOpenRef.current) setTriggerPopup(false);
|
|
2391
|
-
else
|
|
2491
|
+
else openProcesses();
|
|
2392
2492
|
return;
|
|
2393
2493
|
}
|
|
2394
2494
|
|
|
@@ -2396,15 +2496,29 @@ export function App({
|
|
|
2396
2496
|
key.stopPropagation();
|
|
2397
2497
|
if (key.name === "escape") {
|
|
2398
2498
|
setTriggerPopup(false);
|
|
2499
|
+
} else if (processTabForKey(key, processTabRef.current)) {
|
|
2500
|
+
const tab = processTabForKey(key, processTabRef.current)!;
|
|
2501
|
+
processTabRef.current = tab;
|
|
2502
|
+
setProcessTab(tab);
|
|
2399
2503
|
} else if (key.name === "up" || key.name === "down" || key.name === "pageup" || key.name === "pagedown") {
|
|
2400
2504
|
const direction = key.name === "up" || key.name === "pageup" ? -1 : 1;
|
|
2401
2505
|
const steps = key.name === "pageup" || key.name === "pagedown" ? 5 : 1;
|
|
2402
|
-
|
|
2506
|
+
const shellTab = processTabRef.current === "shells";
|
|
2507
|
+
let next = shellTab ? shellCursorRef.current : triggerCursorRef.current;
|
|
2403
2508
|
for (let index = 0; index < steps; index++) {
|
|
2404
|
-
next =
|
|
2509
|
+
next = shellTab
|
|
2510
|
+
? moveProcessSelection(next, shells.length, direction)
|
|
2511
|
+
: moveTriggerSelection(next, triggers.length, direction);
|
|
2512
|
+
}
|
|
2513
|
+
if (shellTab) {
|
|
2514
|
+
shellCursorRef.current = next;
|
|
2515
|
+
setShellCursor(next);
|
|
2516
|
+
} else {
|
|
2517
|
+
triggerCursorRef.current = next;
|
|
2518
|
+
setTriggerCursor(next);
|
|
2405
2519
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2520
|
+
} else if (processTabRef.current === "shells") {
|
|
2521
|
+
if (key.name === "k" || key.sequence === "k") killSelectedShell();
|
|
2408
2522
|
} else {
|
|
2409
2523
|
const action = triggerActionForKey(key, triggers[triggerCursorRef.current]);
|
|
2410
2524
|
if (action) performTriggerAction(action);
|
|
@@ -2870,7 +2984,10 @@ export function App({
|
|
|
2870
2984
|
}
|
|
2871
2985
|
});
|
|
2872
2986
|
|
|
2873
|
-
const
|
|
2987
|
+
const lastProjectedLine = visibleLines[visibleLines.length - 1];
|
|
2988
|
+
const lastLine: Line | undefined = lastProjectedLine?.kind === "tool-summary"
|
|
2989
|
+
? { kind: "text", role: "system", text: lastProjectedLine.text }
|
|
2990
|
+
: lastProjectedLine;
|
|
2874
2991
|
const streamGap = visibleTx.stream
|
|
2875
2992
|
? needsTranscriptGap(lastLine, { kind: "text", role: visibleTx.stream.kind, text: visibleTx.stream.text })
|
|
2876
2993
|
: false;
|
|
@@ -2905,6 +3022,7 @@ export function App({
|
|
|
2905
3022
|
agentCount={agents.length}
|
|
2906
3023
|
runningAgentCount={activeSubagentCount}
|
|
2907
3024
|
maxActiveAgentCount={settings.maxActiveSubagents}
|
|
3025
|
+
runningShellCount={runningShellCount}
|
|
2908
3026
|
activeAgentName={activeAgent?.name}
|
|
2909
3027
|
/>
|
|
2910
3028
|
<WorkingRule
|
|
@@ -2923,11 +3041,24 @@ export function App({
|
|
|
2923
3041
|
stickyStart="bottom"
|
|
2924
3042
|
verticalScrollbarOptions={{ visible: true }}
|
|
2925
3043
|
>
|
|
2926
|
-
{
|
|
2927
|
-
const workingCaret = visibleBusy && !visibleTx.stream && i ===
|
|
3044
|
+
{visibleLines.map((line, i) => {
|
|
3045
|
+
const workingCaret = visibleBusy && !visibleTx.stream && i === visibleLines.length - 1;
|
|
2928
3046
|
const row =
|
|
2929
|
-
line.kind === "tool" ? (
|
|
2930
|
-
<
|
|
3047
|
+
line.kind === "tool-summary" ? (
|
|
3048
|
+
<TextLine
|
|
3049
|
+
theme={theme}
|
|
3050
|
+
syntaxStyle={syntaxStyle}
|
|
3051
|
+
role="system"
|
|
3052
|
+
text={line.text}
|
|
3053
|
+
/>
|
|
3054
|
+
) : line.kind === "tool" ? (
|
|
3055
|
+
<ToolLine
|
|
3056
|
+
theme={theme}
|
|
3057
|
+
syntaxStyle={syntaxStyle}
|
|
3058
|
+
call={line.call}
|
|
3059
|
+
workingCaret={workingCaret}
|
|
3060
|
+
outputMode={outputMode}
|
|
3061
|
+
/>
|
|
2931
3062
|
) : line.kind === "agent-message" ? (
|
|
2932
3063
|
<AgentMessageLine theme={theme} syntaxStyle={syntaxStyle} line={line} />
|
|
2933
3064
|
) : (
|
|
@@ -2944,9 +3075,18 @@ export function App({
|
|
|
2944
3075
|
}
|
|
2945
3076
|
/>
|
|
2946
3077
|
);
|
|
2947
|
-
const
|
|
3078
|
+
const currentGapLine: Line = line.kind === "tool-summary"
|
|
3079
|
+
? { kind: "text", role: "system", text: line.text }
|
|
3080
|
+
: line;
|
|
3081
|
+
const previousProjected = visibleLines[i - 1];
|
|
3082
|
+
const previousGapLine: Line | undefined = previousProjected?.kind === "tool-summary"
|
|
3083
|
+
? { kind: "text", role: "system", text: previousProjected.text }
|
|
3084
|
+
: previousProjected;
|
|
3085
|
+
const gapBefore = needsTranscriptGap(previousGapLine, currentGapLine);
|
|
2948
3086
|
const lineKey =
|
|
2949
|
-
line.kind === "tool"
|
|
3087
|
+
line.kind === "tool-summary"
|
|
3088
|
+
? `tool-summary:${i}:${line.text}`
|
|
3089
|
+
: line.kind === "tool"
|
|
2950
3090
|
? `tool:${line.call.id}`
|
|
2951
3091
|
: line.kind === "agent-message"
|
|
2952
3092
|
? `agent:${line.sender}:${line.recipient}:${i}:${line.text}`
|
|
@@ -2977,7 +3117,7 @@ export function App({
|
|
|
2977
3117
|
) : null}
|
|
2978
3118
|
{visibleTx.pending.some((pending) => !pending.delivered) ? (
|
|
2979
3119
|
<>
|
|
2980
|
-
{(
|
|
3120
|
+
{(visibleLines.length > 0 || visibleTx.stream) ? <Gap /> : null}
|
|
2981
3121
|
{visibleTx.pending.filter((pending) => !pending.delivered).map((pending) => (
|
|
2982
3122
|
<PendingMessageLine
|
|
2983
3123
|
key={pending.id}
|
|
@@ -3125,10 +3265,14 @@ export function App({
|
|
|
3125
3265
|
/>
|
|
3126
3266
|
) : null}
|
|
3127
3267
|
{triggersOpen ? (
|
|
3128
|
-
<
|
|
3268
|
+
<ProcessesPopup
|
|
3129
3269
|
theme={theme}
|
|
3270
|
+
tab={processTab}
|
|
3130
3271
|
triggers={triggers}
|
|
3131
|
-
|
|
3272
|
+
shells={shells}
|
|
3273
|
+
triggerCursor={triggerCursor}
|
|
3274
|
+
shellCursor={shellCursor}
|
|
3275
|
+
shellTail={shells[shellCursor] ? shellTails[shells[shellCursor]!.id] : undefined}
|
|
3132
3276
|
terminalWidth={width}
|
|
3133
3277
|
terminalHeight={height}
|
|
3134
3278
|
/>
|
package/src/check-mode.ts
CHANGED
|
@@ -209,13 +209,14 @@ export function isProcessCheckProposal(value: unknown): value is ProcessCheckPro
|
|
|
209
209
|
if (!value || typeof value !== "object") return false;
|
|
210
210
|
const proposal = value as Partial<ProcessCheckProposal>;
|
|
211
211
|
return proposal.kind === "process"
|
|
212
|
-
&& proposal.source
|
|
212
|
+
&& ["external-trigger", "managed-shell"].includes(proposal.source ?? "")
|
|
213
213
|
&& typeof proposal.executable === "string"
|
|
214
214
|
&& Array.isArray(proposal.args)
|
|
215
215
|
&& proposal.args.every((argument) => typeof argument === "string")
|
|
216
216
|
&& typeof proposal.cwd === "string"
|
|
217
217
|
&& ["create", "start", "resume", "repeat", "invoke-run"].includes(proposal.operation ?? "")
|
|
218
|
-
&& (proposal.triggerName === undefined || typeof proposal.triggerName === "string")
|
|
218
|
+
&& (proposal.triggerName === undefined || typeof proposal.triggerName === "string")
|
|
219
|
+
&& (proposal.shellName === undefined || typeof proposal.shellName === "string");
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
/** Build the exact safety identity. Display-only triggerName is intentionally omitted. */
|
|
@@ -297,7 +298,7 @@ export async function prepareCheck(
|
|
|
297
298
|
const paths = mutation?.changedPaths ?? [];
|
|
298
299
|
const processProposal = isProcessCheckProposal(input) ? input : undefined;
|
|
299
300
|
const summary = processProposal
|
|
300
|
-
? `${processProposal.operation} external trigger process: ${processProposal.executable}`
|
|
301
|
+
? `${processProposal.operation} ${processProposal.source === "managed-shell" ? "managed shell" : "external trigger"} process: ${processProposal.executable}`
|
|
301
302
|
: toolName === "bash"
|
|
302
303
|
? `Run ${bash!.stages.length} shell stage${bash!.stages.length === 1 ? "" : "s"}`
|
|
303
304
|
: `Change ${paths.length} project file${paths.length === 1 ? "" : "s"} (+${mutation!.additions} −${mutation!.removals})`;
|
|
@@ -325,6 +326,7 @@ export async function prepareCheck(
|
|
|
325
326
|
args: processProposal.args,
|
|
326
327
|
cwd: processProposal.cwd,
|
|
327
328
|
triggerName: processProposal.triggerName,
|
|
329
|
+
shellName: processProposal.shellName,
|
|
328
330
|
analysis: bash,
|
|
329
331
|
} : undefined,
|
|
330
332
|
proposedMutation: mutation ? {
|
|
@@ -710,6 +712,8 @@ export type ExternalTriggerSafetyChecker = (
|
|
|
710
712
|
signal?: AbortSignal,
|
|
711
713
|
) => Promise<void>;
|
|
712
714
|
|
|
715
|
+
export type ManagedShellSafetyChecker = ExternalTriggerSafetyChecker;
|
|
716
|
+
|
|
713
717
|
/** Create the process safety callback used by TriggerManager. */
|
|
714
718
|
export function createExternalTriggerSafetyChecker(
|
|
715
719
|
runtime: CheckerRuntime,
|
|
@@ -733,6 +737,9 @@ export function createExternalTriggerSafetyChecker(
|
|
|
733
737
|
};
|
|
734
738
|
}
|
|
735
739
|
|
|
740
|
+
/** Create the structured process safety callback used by ShellManager. */
|
|
741
|
+
export const createManagedShellSafetyChecker = createExternalTriggerSafetyChecker;
|
|
742
|
+
|
|
736
743
|
export async function verifyToolCall(runtime: CheckerRuntime, call: ToolCheck): Promise<ToolBlock | undefined> {
|
|
737
744
|
const evaluation = await evaluateToolCall(runtime, call);
|
|
738
745
|
return evaluation.decision === "allow" ? undefined : { block: true, reason: evaluation.reason };
|
package/src/check-policy.ts
CHANGED
|
@@ -144,7 +144,7 @@ export type ProcessCheckOperation = "create" | "start" | "resume" | "repeat" | "
|
|
|
144
144
|
|
|
145
145
|
export type ProcessCheckProposal = {
|
|
146
146
|
kind: "process";
|
|
147
|
-
source: "external-trigger";
|
|
147
|
+
source: "external-trigger" | "managed-shell";
|
|
148
148
|
executable: string;
|
|
149
149
|
/** Process arguments. The executable is not included. */
|
|
150
150
|
args: readonly string[];
|
|
@@ -152,6 +152,8 @@ export type ProcessCheckProposal = {
|
|
|
152
152
|
operation: ProcessCheckOperation;
|
|
153
153
|
/** Display context only. This field is not part of the safety identity. */
|
|
154
154
|
triggerName?: string;
|
|
155
|
+
/** Display context only. This field is not part of the safety identity. */
|
|
156
|
+
shellName?: string;
|
|
155
157
|
};
|
|
156
158
|
|
|
157
159
|
export type AnalyzeExecutablePolicyOptions = Pick<ProcessCheckProposal, "executable" | "args" | "cwd"> & {
|
package/src/commands.ts
CHANGED
|
@@ -39,7 +39,11 @@ export const COMMANDS: Command[] = [
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
name: "/triggers",
|
|
42
|
-
description: "
|
|
42
|
+
description: "Open Processes on the Triggers tab",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "/processes",
|
|
46
|
+
description: "Manage external triggers and shells",
|
|
43
47
|
},
|
|
44
48
|
{
|
|
45
49
|
name: "/worktree",
|
package/src/help-popup.tsx
CHANGED
|
@@ -59,7 +59,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
59
59
|
["/news", "Open recent answers (News)"],
|
|
60
60
|
["/stats", "Show session statistics"],
|
|
61
61
|
["/check-path", "Manage extra Check mode paths"],
|
|
62
|
-
["/
|
|
62
|
+
["/processes", "Manage triggers and shells"],
|
|
63
63
|
["/worktree", "Create a managed worktree"],
|
|
64
64
|
["Tab", "Complete a command preview"],
|
|
65
65
|
],
|
|
@@ -69,7 +69,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
69
69
|
controls: [
|
|
70
70
|
["Ctrl+P", "Open Settings"],
|
|
71
71
|
["Ctrl+N", "Open News; n/p jump"],
|
|
72
|
-
["Ctrl+T", "Open
|
|
72
|
+
["Ctrl+T", "Open Processes"],
|
|
73
73
|
["Ctrl+End", "Scroll transcript to the end"],
|
|
74
74
|
["/ in Settings", "Focus settings search"],
|
|
75
75
|
["Esc", "Close; twice to cancel work"],
|
package/src/main.tsx
CHANGED
|
@@ -52,6 +52,16 @@ import {
|
|
|
52
52
|
filesystemSandboxExtension,
|
|
53
53
|
} from "./filesystem-sandbox";
|
|
54
54
|
import { SessionStatsManager } from "./session-stats";
|
|
55
|
+
import { ShellManager } from "./shells/manager";
|
|
56
|
+
import {
|
|
57
|
+
NodeShellFileOperations,
|
|
58
|
+
NodeShellProcessAdapter,
|
|
59
|
+
systemShellClock,
|
|
60
|
+
} from "./shells/process";
|
|
61
|
+
import {
|
|
62
|
+
ManagedShellLifecycleController,
|
|
63
|
+
lifecycleEventFromSnapshot,
|
|
64
|
+
} from "./shells/lifecycle";
|
|
55
65
|
|
|
56
66
|
export async function start(options: StartupOptions): Promise<void> {
|
|
57
67
|
mkdirSync(AGENT_DIR, { recursive: true });
|
|
@@ -100,6 +110,33 @@ export async function start(options: StartupOptions): Promise<void> {
|
|
|
100
110
|
});
|
|
101
111
|
});
|
|
102
112
|
let subagentManager!: SubagentManager;
|
|
113
|
+
const shellLifecycle = new ManagedShellLifecycleController(
|
|
114
|
+
{
|
|
115
|
+
append: (_owner, _customType, data) =>
|
|
116
|
+
subagentManager.persistManagedShellEvent(data as any),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
deliver: (message) => subagentManager.deliverManagedShellCompletion(message.details),
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
const startedShells = new Set<string>();
|
|
123
|
+
const shellManager = new ShellManager({
|
|
124
|
+
process: new NodeShellProcessAdapter(),
|
|
125
|
+
files: new NodeShellFileOperations(),
|
|
126
|
+
clock: systemShellClock,
|
|
127
|
+
async onCompleted(snapshot) {
|
|
128
|
+
const output = await shellManager.getOutput(snapshot.id, { lineLimit: 200 }).catch(() => undefined);
|
|
129
|
+
const event = lifecycleEventFromSnapshot(snapshot, output?.tail);
|
|
130
|
+
await shellLifecycle.recordExit(event, snapshot.state === "terminated");
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
shellManager.subscribe((event) => {
|
|
134
|
+
if (event.type !== "changed"
|
|
135
|
+
|| !["starting", "running"].includes(event.snapshot.state)
|
|
136
|
+
|| startedShells.has(event.snapshot.id)) return;
|
|
137
|
+
startedShells.add(event.snapshot.id);
|
|
138
|
+
void shellLifecycle.record(lifecycleEventFromSnapshot(event.snapshot));
|
|
139
|
+
});
|
|
103
140
|
const triggerManager = new TriggerManager({
|
|
104
141
|
process: new NodeTriggerProcessAdapter(),
|
|
105
142
|
clock: systemTriggerClock,
|
|
@@ -151,6 +188,7 @@ export async function start(options: StartupOptions): Promise<void> {
|
|
|
151
188
|
messageCacheController,
|
|
152
189
|
statsManager,
|
|
153
190
|
triggerManager,
|
|
191
|
+
shellManager,
|
|
154
192
|
childExtensionFactories: [
|
|
155
193
|
identityExtension,
|
|
156
194
|
writingStyleExtension,
|
|
@@ -239,6 +277,7 @@ export async function start(options: StartupOptions): Promise<void> {
|
|
|
239
277
|
selectionClipboard.dispose();
|
|
240
278
|
cleanupPendingImages();
|
|
241
279
|
},
|
|
280
|
+
shutdownShells: () => shellManager.shutdown(),
|
|
242
281
|
shutdownTriggers: () => triggerManager.shutdown(),
|
|
243
282
|
dispose: () => sessionRuntime.dispose(),
|
|
244
283
|
destroy: async () => {
|