pum-agent 0.2.14-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 -17
- 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/manager.ts +150 -6
- package/src/tool-groups.ts +17 -1
- package/src/tool-line.ts +9 -0
- package/src/tool-preview.ts +133 -0
- package/src/transcript-output.ts +30 -0
- package/src/transcript.tsx +150 -0
- package/src/triggers/popup.tsx +3 -1
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"
|
|
@@ -1210,6 +1286,9 @@ export function App({
|
|
|
1210
1286
|
? messageCacheDetail(event.result)
|
|
1211
1287
|
: undefined,
|
|
1212
1288
|
exitCode: bashResult.exitCode,
|
|
1289
|
+
result: event.result,
|
|
1290
|
+
isError: event.isError,
|
|
1291
|
+
...(preview ? { preview } : {}),
|
|
1213
1292
|
});
|
|
1214
1293
|
break;
|
|
1215
1294
|
}
|
|
@@ -1435,6 +1514,10 @@ export function App({
|
|
|
1435
1514
|
update({ workingRuleAnimation: next });
|
|
1436
1515
|
};
|
|
1437
1516
|
|
|
1517
|
+
const stepOutputMode = (step: number) => {
|
|
1518
|
+
update({ outputMode: cycleOutputMode(settings.outputMode, step) });
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1438
1521
|
const openLogin = () => {
|
|
1439
1522
|
settingsOpenRef.current = false;
|
|
1440
1523
|
setSettingsOpen(false);
|
|
@@ -1817,10 +1900,11 @@ export function App({
|
|
|
1817
1900
|
const loginCommand = trimmed === "/login";
|
|
1818
1901
|
const checkPathCommand = /^\/check-path(?:\s|$)/.test(trimmed);
|
|
1819
1902
|
const triggersCommand = trimmed === "/triggers";
|
|
1903
|
+
const processesCommand = trimmed === "/processes";
|
|
1820
1904
|
const newsCommand = trimmed === "/news";
|
|
1821
1905
|
const statsCommand = trimmed === "/stats";
|
|
1822
1906
|
const worktreeCommand = /^\/worktree(?:\s+([a-zA-Z0-9_-]+))?$/.exec(trimmed);
|
|
1823
|
-
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;
|
|
1824
1908
|
editingStashIndex.current = null;
|
|
1825
1909
|
|
|
1826
1910
|
if (historyCommand) {
|
|
@@ -1838,6 +1922,11 @@ export function App({
|
|
|
1838
1922
|
openTriggers();
|
|
1839
1923
|
return true;
|
|
1840
1924
|
}
|
|
1925
|
+
if (processesCommand) {
|
|
1926
|
+
setEditorText("");
|
|
1927
|
+
openProcesses();
|
|
1928
|
+
return true;
|
|
1929
|
+
}
|
|
1841
1930
|
if (newsCommand) {
|
|
1842
1931
|
setEditorText("");
|
|
1843
1932
|
openNews();
|
|
@@ -2108,6 +2197,16 @@ export function App({
|
|
|
2108
2197
|
}));
|
|
2109
2198
|
};
|
|
2110
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
|
+
|
|
2111
2210
|
const stepCheckMode = (step: number) => {
|
|
2112
2211
|
const index = CHECK_MODE_PROFILES.indexOf(settings.checkMode);
|
|
2113
2212
|
update({ checkMode: CHECK_MODE_PROFILES[(index + step + CHECK_MODE_PROFILES.length) % CHECK_MODE_PROFILES.length]! });
|
|
@@ -2124,6 +2223,7 @@ export function App({
|
|
|
2124
2223
|
providers: { enter: openLogin },
|
|
2125
2224
|
animations: { step: () => update({ animations: !settings.animations }) },
|
|
2126
2225
|
workingRuleAnimation: { step: stepWorkingRuleAnimation },
|
|
2226
|
+
outputMode: { step: stepOutputMode },
|
|
2127
2227
|
webSearch: { step: () => update({ webSearch: !settings.webSearch }) },
|
|
2128
2228
|
writingStyle: { step: stepWritingStyle },
|
|
2129
2229
|
explanationStrength: { step: stepExplanationStrength },
|
|
@@ -2166,6 +2266,7 @@ export function App({
|
|
|
2166
2266
|
providers: "login and custom setup ›",
|
|
2167
2267
|
animations: `‹ ${settings.animations ? "on" : "off"} ›`,
|
|
2168
2268
|
workingRuleAnimation: `‹ ${settings.workingRuleAnimation} ›${settings.workingRuleAnimation === "off" ? "" : animationUnavailable}`,
|
|
2269
|
+
outputMode: `‹ ${settings.outputMode ?? "default"} ›`,
|
|
2169
2270
|
webSearch: `‹ ${settings.webSearch ? "on" : "off"} ›${searchProviders.length ? "" : " (not on provider)"}`,
|
|
2170
2271
|
writingStyle: `‹ ${settings.writingStyle} ›`,
|
|
2171
2272
|
explanationStrength: `‹ ${settings.explanationStrength} ›`,
|
|
@@ -2387,7 +2488,7 @@ export function App({
|
|
|
2387
2488
|
if (key.ctrl && key.name === "t") {
|
|
2388
2489
|
key.stopPropagation();
|
|
2389
2490
|
if (triggersOpenRef.current) setTriggerPopup(false);
|
|
2390
|
-
else
|
|
2491
|
+
else openProcesses();
|
|
2391
2492
|
return;
|
|
2392
2493
|
}
|
|
2393
2494
|
|
|
@@ -2395,15 +2496,29 @@ export function App({
|
|
|
2395
2496
|
key.stopPropagation();
|
|
2396
2497
|
if (key.name === "escape") {
|
|
2397
2498
|
setTriggerPopup(false);
|
|
2499
|
+
} else if (processTabForKey(key, processTabRef.current)) {
|
|
2500
|
+
const tab = processTabForKey(key, processTabRef.current)!;
|
|
2501
|
+
processTabRef.current = tab;
|
|
2502
|
+
setProcessTab(tab);
|
|
2398
2503
|
} else if (key.name === "up" || key.name === "down" || key.name === "pageup" || key.name === "pagedown") {
|
|
2399
2504
|
const direction = key.name === "up" || key.name === "pageup" ? -1 : 1;
|
|
2400
2505
|
const steps = key.name === "pageup" || key.name === "pagedown" ? 5 : 1;
|
|
2401
|
-
|
|
2506
|
+
const shellTab = processTabRef.current === "shells";
|
|
2507
|
+
let next = shellTab ? shellCursorRef.current : triggerCursorRef.current;
|
|
2402
2508
|
for (let index = 0; index < steps; index++) {
|
|
2403
|
-
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);
|
|
2404
2519
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2520
|
+
} else if (processTabRef.current === "shells") {
|
|
2521
|
+
if (key.name === "k" || key.sequence === "k") killSelectedShell();
|
|
2407
2522
|
} else {
|
|
2408
2523
|
const action = triggerActionForKey(key, triggers[triggerCursorRef.current]);
|
|
2409
2524
|
if (action) performTriggerAction(action);
|
|
@@ -2869,7 +2984,10 @@ export function App({
|
|
|
2869
2984
|
}
|
|
2870
2985
|
});
|
|
2871
2986
|
|
|
2872
|
-
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;
|
|
2873
2991
|
const streamGap = visibleTx.stream
|
|
2874
2992
|
? needsTranscriptGap(lastLine, { kind: "text", role: visibleTx.stream.kind, text: visibleTx.stream.text })
|
|
2875
2993
|
: false;
|
|
@@ -2904,6 +3022,7 @@ export function App({
|
|
|
2904
3022
|
agentCount={agents.length}
|
|
2905
3023
|
runningAgentCount={activeSubagentCount}
|
|
2906
3024
|
maxActiveAgentCount={settings.maxActiveSubagents}
|
|
3025
|
+
runningShellCount={runningShellCount}
|
|
2907
3026
|
activeAgentName={activeAgent?.name}
|
|
2908
3027
|
/>
|
|
2909
3028
|
<WorkingRule
|
|
@@ -2922,11 +3041,24 @@ export function App({
|
|
|
2922
3041
|
stickyStart="bottom"
|
|
2923
3042
|
verticalScrollbarOptions={{ visible: true }}
|
|
2924
3043
|
>
|
|
2925
|
-
{
|
|
2926
|
-
const workingCaret = visibleBusy && !visibleTx.stream && i ===
|
|
3044
|
+
{visibleLines.map((line, i) => {
|
|
3045
|
+
const workingCaret = visibleBusy && !visibleTx.stream && i === visibleLines.length - 1;
|
|
2927
3046
|
const row =
|
|
2928
|
-
line.kind === "tool" ? (
|
|
2929
|
-
<
|
|
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
|
+
/>
|
|
2930
3062
|
) : line.kind === "agent-message" ? (
|
|
2931
3063
|
<AgentMessageLine theme={theme} syntaxStyle={syntaxStyle} line={line} />
|
|
2932
3064
|
) : (
|
|
@@ -2943,9 +3075,18 @@ export function App({
|
|
|
2943
3075
|
}
|
|
2944
3076
|
/>
|
|
2945
3077
|
);
|
|
2946
|
-
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);
|
|
2947
3086
|
const lineKey =
|
|
2948
|
-
line.kind === "tool"
|
|
3087
|
+
line.kind === "tool-summary"
|
|
3088
|
+
? `tool-summary:${i}:${line.text}`
|
|
3089
|
+
: line.kind === "tool"
|
|
2949
3090
|
? `tool:${line.call.id}`
|
|
2950
3091
|
: line.kind === "agent-message"
|
|
2951
3092
|
? `agent:${line.sender}:${line.recipient}:${i}:${line.text}`
|
|
@@ -2976,7 +3117,7 @@ export function App({
|
|
|
2976
3117
|
) : null}
|
|
2977
3118
|
{visibleTx.pending.some((pending) => !pending.delivered) ? (
|
|
2978
3119
|
<>
|
|
2979
|
-
{(
|
|
3120
|
+
{(visibleLines.length > 0 || visibleTx.stream) ? <Gap /> : null}
|
|
2980
3121
|
{visibleTx.pending.filter((pending) => !pending.delivered).map((pending) => (
|
|
2981
3122
|
<PendingMessageLine
|
|
2982
3123
|
key={pending.id}
|
|
@@ -3124,10 +3265,14 @@ export function App({
|
|
|
3124
3265
|
/>
|
|
3125
3266
|
) : null}
|
|
3126
3267
|
{triggersOpen ? (
|
|
3127
|
-
<
|
|
3268
|
+
<ProcessesPopup
|
|
3128
3269
|
theme={theme}
|
|
3270
|
+
tab={processTab}
|
|
3129
3271
|
triggers={triggers}
|
|
3130
|
-
|
|
3272
|
+
shells={shells}
|
|
3273
|
+
triggerCursor={triggerCursor}
|
|
3274
|
+
shellCursor={shellCursor}
|
|
3275
|
+
shellTail={shells[shellCursor] ? shellTails[shells[shellCursor]!.id] : undefined}
|
|
3131
3276
|
terminalWidth={width}
|
|
3132
3277
|
terminalHeight={height}
|
|
3133
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 () => {
|