pum-agent 0.1.2-beta.1 → 0.2.0-beta.2
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 +58 -11
- package/package.json +1 -1
- package/src/agent-selector.tsx +18 -18
- package/src/app.tsx +125 -11
- package/src/browser-launch.ts +68 -0
- package/src/check-approval-popup.tsx +10 -11
- package/src/check-approvals.ts +22 -10
- package/src/check-mode.ts +209 -24
- package/src/check-mutation.ts +32 -0
- package/src/check-policy.ts +112 -20
- package/src/cli.ts +75 -0
- package/src/clipboard.ts +152 -0
- package/src/help-popup.tsx +21 -16
- package/src/image-paste.ts +15 -5
- package/src/index.tsx +15 -217
- package/src/login-controller.ts +97 -7
- package/src/login-popup.tsx +71 -27
- package/src/main.tsx +239 -0
- package/src/popup-frame.tsx +93 -0
- package/src/questionnaire-popup.tsx +10 -15
- package/src/queue-recall.ts +57 -0
- package/src/session-history-popup.tsx +16 -16
- package/src/settings-popup.tsx +196 -104
- package/src/shutdown.ts +14 -8
- package/src/status-bar.tsx +252 -87
- package/src/status-metadata.ts +4 -2
- package/src/subagents/manager.ts +81 -7
- package/src/subagents/spawn-preview-popup.tsx +106 -0
- package/src/subagents/spawn-preview.ts +126 -0
- package/src/syntax.ts +15 -1
- package/src/theme.ts +11 -0
- package/src/tool-line.ts +1 -1
- package/src/transcript.tsx +25 -7
- package/src/triggers/manager.ts +4 -58
- package/src/triggers/popup.tsx +15 -21
- package/src/triggers/tools.ts +6 -11
- package/src/triggers/types.ts +1 -5
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ The following screens are real OpenTUI renders captured through `tmux`. A local
|
|
|
45
45
|
- **Parallel subagents:** Persistent agents work in isolated Git worktrees, communicate durably, and report lifecycle transitions to their direct spawners.
|
|
46
46
|
- **Prompt control:** Steer active work, answer model questionnaires, use an ownership-aware message cache, attach clipboard images, and resume sessions with metadata-rich history.
|
|
47
47
|
- **External triggers:** Supervise background commands such as `gh run watch` and automatically wake the exact target agent when they exit.
|
|
48
|
-
- **Provider choice:**
|
|
48
|
+
- **Provider choice:** Search the providers exposed by pi, or add an OpenAI-compatible custom endpoint.
|
|
49
49
|
- **Optional safeguards:** Use strict, balanced, or ask Check mode for `bash`, `edit`, `apply_patch`, and external-trigger process proposals.
|
|
50
50
|
- **Terminal-first appearance:** Nine themes, semantic color overrides, Unicode glyphs, and optional animation.
|
|
51
51
|
|
|
@@ -74,7 +74,7 @@ bun run login
|
|
|
74
74
|
bun run start
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
PUM opens the login panel automatically when no provider is available. Use `/login` later to add or update a provider.
|
|
77
|
+
PUM opens the login panel automatically when no provider is available. Use `/login` later to add or update a provider. During browser-based login, PUM opens credential-free HTTP(S) authentication URLs with the platform browser. The URL remains selectable when automatic launch is unavailable.
|
|
78
78
|
|
|
79
79
|
Resume the latest session for the current directory:
|
|
80
80
|
|
|
@@ -91,11 +91,30 @@ pum
|
|
|
91
91
|
|
|
92
92
|
The package is named `pum-agent` because the bare `pum` name is already owned. The installed command is still `pum`.
|
|
93
93
|
|
|
94
|
+
## Command-line options
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
pum [options]
|
|
98
|
+
pum login [options]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Option or command | Action |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `-h`, `--help` | Print the command-line manual and exit |
|
|
104
|
+
| `-v`, `--version` | Print the exact `pum-agent` package version and exit |
|
|
105
|
+
| `-r`, `--resume` | Resume the latest session for the current directory |
|
|
106
|
+
| `login` | Start PUM with the provider login panel open |
|
|
107
|
+
|
|
108
|
+
Help and version handling do not initialize the TUI, configuration, credentials, or sessions. Unknown options and commands return an error and a help hint.
|
|
109
|
+
|
|
110
|
+
Set `PUM_DIR` to override PUM's complete configuration and data directory. Run `pum --help` for a concise directory summary. Enter `?` on an empty in-app prompt to see all controls.
|
|
111
|
+
|
|
94
112
|
## Essential controls
|
|
95
113
|
|
|
96
114
|
| Key | Action |
|
|
97
115
|
|---|---|
|
|
98
116
|
| `Enter` | Send a prompt, or steer the selected working agent |
|
|
117
|
+
| `↑` on an empty prompt | Recall the newest queued user message for the selected agent |
|
|
99
118
|
| `Ctrl+Enter` / `Shift+Enter` | Insert a new line |
|
|
100
119
|
| `Alt+Enter` | Stash the prompt without sending |
|
|
101
120
|
| `Tab` | Open the prompt stash on an empty input |
|
|
@@ -112,6 +131,28 @@ The package is named `pum-agent` because the bare `pum` name is already owned. T
|
|
|
112
131
|
|
|
113
132
|
Useful commands include `/login`, `/history`, `/triggers`, `/clear`, `/compress`, and `/worktree`.
|
|
114
133
|
|
|
134
|
+
### Copy transcript text
|
|
135
|
+
|
|
136
|
+
Drag across transcript text with the left mouse button. PUM copies the completed selection when you release the button.
|
|
137
|
+
|
|
138
|
+
- On local Windows, PUM first uses the native clipboard module. PUM then tries `clip.exe`.
|
|
139
|
+
- On local macOS, PUM first uses the native clipboard module. PUM then tries `pbcopy`.
|
|
140
|
+
- On local Linux, PUM tries `wl-copy`, `xclip`, or `xsel` when the matching display is available.
|
|
141
|
+
- Over SSH or Mosh, PUM sends OSC 52 through OpenTUI. OpenTUI wraps OSC 52 for detected `tmux` sessions.
|
|
142
|
+
|
|
143
|
+
Windows Terminal accepts OSC 52 from remote sessions. The terminal can still ask for clipboard-write approval.
|
|
144
|
+
|
|
145
|
+
For `tmux`, enable clipboard integration and passthrough when the server configuration blocks OSC 52:
|
|
146
|
+
|
|
147
|
+
```tmux
|
|
148
|
+
set -g set-clipboard on
|
|
149
|
+
set -g allow-passthrough on
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Reload the `tmux` configuration after this change. Use the terminal's Shift-drag selection as a manual fallback.
|
|
153
|
+
|
|
154
|
+
PUM limits remote OSC 52 payloads to 100,000 Base64 characters. This limit prevents large selections from corrupting terminal output.
|
|
155
|
+
|
|
115
156
|
## Parallel subagents
|
|
116
157
|
|
|
117
158
|
PUM runs up to 10 active subagents by default. Configure a limit from 1 through 25 in Settings. Only starting and running agents count toward the limit. Each subagent has these resources:
|
|
@@ -125,6 +166,10 @@ Select a range of stashed prompts and press `Enter`. The main agent can group re
|
|
|
125
166
|
|
|
126
167
|
Use `Ctrl+L` to select an agent transcript. Input then goes to that agent. Finished or interrupted agents remain available until PUM merges or removes them.
|
|
127
168
|
|
|
169
|
+
The public `spawn_subagent` tool accepts `preview: true`. PUM then shows the exact child task before it creates any worktree or session. Press `Enter` to approve. An optional note becomes a separate visible user instruction to the new child. Press `Esc` to cancel without creating a child. Cancellation discards the preview note because no child exists. PUM preserves the existing parent transcript draft.
|
|
170
|
+
|
|
171
|
+
Press `↑` on an empty single-line prompt to recall the newest queued user-authored message for the selected transcript. PUM removes the message from the authoritative queue before restoring its text. PUM does not recall inter-agent, trigger, lifecycle, cache, delivered, or image-bearing messages.
|
|
172
|
+
|
|
128
173
|
Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM persists completion intent and delivery state so interrupted notification delivery can resume without duplicate completion messages.
|
|
129
174
|
|
|
130
175
|
## Tools and safeguards
|
|
@@ -145,9 +190,9 @@ The `message_cache_send` tool accepts stable entry IDs. Single entries use the s
|
|
|
145
190
|
|
|
146
191
|
### External triggers
|
|
147
192
|
|
|
148
|
-
The trigger tools create process-local supervised commands with an executable and argument array. Trigger definitions can be listed, inspected, paused, resumed, cancelled, run manually
|
|
193
|
+
The trigger tools create process-local supervised commands with an executable and argument array. Trigger definitions can be listed, inspected, paused, resumed, cancelled, or run manually. Definitions disappear when PUM exits.
|
|
149
194
|
|
|
150
|
-
Use `Ctrl+T` or `/triggers` to inspect active definitions. PUM limits definitions, pending deliveries, output size,
|
|
195
|
+
Use `Ctrl+T` or `/triggers` to inspect active definitions. PUM limits definitions, pending deliveries, output size, run counts, repeat frequency, and lifetime. Output goes to a private temporary file and is removed after the triggered turn settles.
|
|
151
196
|
|
|
152
197
|
Trigger events target one exact main or retained child session. A missing session or child cancels its definitions instead of redirecting them. Check mode evaluates each process proposal without flattening its argument boundaries into shell text.
|
|
153
198
|
|
|
@@ -160,16 +205,18 @@ Trigger events target one exact main or retained child session. A missing sessio
|
|
|
160
205
|
Select a Check mode profile in `Ctrl+P`. It applies to `bash`, `edit`, `apply_patch`, and external-trigger process execution:
|
|
161
206
|
|
|
162
207
|
- **Strict:** Run deterministic hard rules, then require a clear verifier approval.
|
|
163
|
-
- **Balanced:**
|
|
164
|
-
- **Ask:**
|
|
208
|
+
- **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
|
|
209
|
+
- **Ask:** Show the approval popup for every checked call that passes hard rules, unless an exact session or project approval already matches. A verifier `SAFE`, unclear, error, or unavailable result still requires approval.
|
|
210
|
+
|
|
211
|
+
Every active profile hard-blocks project escape, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only exception is a deterministic match for direct main-agent `npm publish` or `npm dist-tag add`. The verifier category does not control this exception. The exception still requires explicit popup approval. Managed subagents cannot use the exception.
|
|
165
212
|
|
|
166
|
-
|
|
213
|
+
For `edit` and `apply_patch`, PUM validates the complete proposed change before any mutation. Review data includes the unified diff, changed paths, line counts, sensitivity flags, project containment, and full-content SHA-256. Invalid, stale, malformed, escaping, or incompletely analyzed input blocks the call. Patch length alone does not block a valid Balanced call.
|
|
167
214
|
|
|
168
|
-
|
|
215
|
+
Ask mode can allow an exact call once, for the current session, or for the current project. Approvals match the authoritative main or child identity, tool, verifier model, project, and canonical complete input. Chat text is not approval. Use **Clear approvals** in Settings to remove project approvals.
|
|
169
216
|
|
|
170
|
-
|
|
217
|
+
The verifier uses a structured decision schema. One unclear response can receive one adjudication under the shared 15-second watchdog. Strict blocks malformed replies, errors, aborts, and timeouts. Balanced allows a fully validated call after an unclear, unavailable, failed, or timed-out review. Balanced still blocks explicit verifier `UNSAFE`, aborts, deterministic suspicious findings, malformed structures, and incomplete analysis. Ask requires the popup after hard rules for verifier `SAFE`, unclear, error, and unavailable results. Check mode is off by default and is not a sandbox.
|
|
171
218
|
|
|
172
|
-
|
|
219
|
+
Verifier prompts stay bounded. For an oversized Balanced review, PUM sends complete validation metadata, counts, findings, and SHA-256 digests. PUM does not send a raw prefix or suffix as if it were complete. Strict and Ask keep their fail-closed oversized-input behavior.
|
|
173
220
|
|
|
174
221
|
### Hosted web search
|
|
175
222
|
|
|
@@ -239,7 +286,7 @@ Use a throwaway `PUM_DIR` for local integration tests. Capture TUI output throug
|
|
|
239
286
|
|
|
240
287
|
## Release status
|
|
241
288
|
|
|
242
|
-
PUM `0.
|
|
289
|
+
PUM `0.2` is in beta. Interfaces and persisted formats can still change before the stable release. Review the release notes before upgrading sessions or custom configuration.
|
|
243
290
|
|
|
244
291
|
## License
|
|
245
292
|
|
package/package.json
CHANGED
package/src/agent-selector.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type StatusMetadataValues,
|
|
11
11
|
} from "./status-metadata";
|
|
12
12
|
import type { Theme } from "./theme";
|
|
13
|
+
import { PopupFrame } from "./popup-frame";
|
|
13
14
|
import type { SubagentSnapshot } from "./subagents/types";
|
|
14
15
|
|
|
15
16
|
export type AgentTreeRow = {
|
|
@@ -125,12 +126,19 @@ export function AgentSelectorPopup({
|
|
|
125
126
|
const { width, height } = useTerminalDimensions();
|
|
126
127
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
127
128
|
const narrow = width < 50;
|
|
128
|
-
const popupWidth = narrow ? Math.max(
|
|
129
|
+
const popupWidth = narrow ? Math.max(1, width - 2) : Math.max(1, Math.floor(width * 0.7));
|
|
129
130
|
const popupColumns = Math.max(8, popupWidth - 5);
|
|
130
|
-
const popupHeight = Math.min(
|
|
131
|
+
const popupHeight = Math.max(1, Math.min(
|
|
132
|
+
height,
|
|
131
133
|
rows.length + (narrow ? 10 : 7),
|
|
132
134
|
Math.max(7, Math.floor(height * (narrow ? 0.85 : 0.7))),
|
|
133
|
-
);
|
|
135
|
+
));
|
|
136
|
+
const geometry = {
|
|
137
|
+
top: narrow ? Math.min(1, Math.max(0, height - popupHeight)) : Math.max(0, Math.floor(height * 0.15)),
|
|
138
|
+
left: narrow ? Math.min(1, Math.max(0, width - popupWidth)) : Math.max(0, Math.floor(width * 0.15)),
|
|
139
|
+
width: popupWidth,
|
|
140
|
+
height: popupHeight,
|
|
141
|
+
};
|
|
134
142
|
|
|
135
143
|
useEffect(() => {
|
|
136
144
|
const timer = setTimeout(() => {
|
|
@@ -140,21 +148,13 @@ export function AgentSelectorPopup({
|
|
|
140
148
|
}, [cursor]);
|
|
141
149
|
|
|
142
150
|
return (
|
|
143
|
-
<
|
|
151
|
+
<PopupFrame
|
|
152
|
+
theme={theme}
|
|
153
|
+
terminalWidth={width}
|
|
154
|
+
terminalHeight={height}
|
|
155
|
+
geometry={geometry}
|
|
156
|
+
zIndex={100}
|
|
144
157
|
title=" Agents "
|
|
145
|
-
style={{
|
|
146
|
-
position: "absolute",
|
|
147
|
-
top: narrow ? 1 : "15%",
|
|
148
|
-
left: narrow ? 1 : "15%",
|
|
149
|
-
width: popupWidth,
|
|
150
|
-
height: popupHeight,
|
|
151
|
-
zIndex: 100,
|
|
152
|
-
border: true,
|
|
153
|
-
borderColor: theme.border,
|
|
154
|
-
backgroundColor: theme.popupBg,
|
|
155
|
-
flexDirection: "column",
|
|
156
|
-
padding: 1,
|
|
157
|
-
}}
|
|
158
158
|
>
|
|
159
159
|
<scrollbox
|
|
160
160
|
ref={scrollRef}
|
|
@@ -212,6 +212,6 @@ export function AgentSelectorPopup({
|
|
|
212
212
|
wrapMode="word"
|
|
213
213
|
style={{ flexShrink: 0, width: "100%" }}
|
|
214
214
|
/>
|
|
215
|
-
</
|
|
215
|
+
</PopupFrame>
|
|
216
216
|
);
|
|
217
217
|
}
|
package/src/app.tsx
CHANGED
|
@@ -80,6 +80,9 @@ import {
|
|
|
80
80
|
type PendingImage,
|
|
81
81
|
} from "./image-paste";
|
|
82
82
|
import type { SubagentManager } from "./subagents/manager";
|
|
83
|
+
import type { SpawnPreviewManager } from "./subagents/spawn-preview";
|
|
84
|
+
import { SpawnPreviewPopup } from "./subagents/spawn-preview-popup";
|
|
85
|
+
import { recallNewestQueuedUserMessage } from "./queue-recall";
|
|
83
86
|
import { runWorktreeCommand } from "./worktree-command";
|
|
84
87
|
import { CANCEL_WINDOW_MS, confirmsCancellation } from "./cancel-confirmation";
|
|
85
88
|
import { buildStashBatchPrompt, selectedRange } from "./stash-batch";
|
|
@@ -329,6 +332,7 @@ export function App({
|
|
|
329
332
|
searchProviders,
|
|
330
333
|
subagentManager,
|
|
331
334
|
questionnaireManager,
|
|
335
|
+
spawnPreviewManager,
|
|
332
336
|
loginRequired = false,
|
|
333
337
|
promptHistoryStore = DEFAULT_PROMPT_HISTORY_STORE,
|
|
334
338
|
promptStashStore = DEFAULT_PROMPT_STASH_STORE,
|
|
@@ -349,6 +353,7 @@ export function App({
|
|
|
349
353
|
searchProviders: string[];
|
|
350
354
|
subagentManager: SubagentManager;
|
|
351
355
|
questionnaireManager?: QuestionnaireManager;
|
|
356
|
+
spawnPreviewManager?: SpawnPreviewManager;
|
|
352
357
|
loginRequired?: boolean;
|
|
353
358
|
promptHistoryStore?: PromptHistoryStore;
|
|
354
359
|
promptStashStore?: PromptStashStore;
|
|
@@ -406,10 +411,14 @@ export function App({
|
|
|
406
411
|
const [agentElapsedSec, setAgentElapsedSec] = useState(0);
|
|
407
412
|
const [loginOpen, setLoginOpen] = useState(loginRequired);
|
|
408
413
|
const [, setQuestionnaireRevision] = useState(0);
|
|
414
|
+
const [, setSpawnPreviewRevision] = useState(0);
|
|
409
415
|
const [loginPage, setLoginPage] = useState<LoginPage>(() => ({
|
|
410
416
|
kind: "providers",
|
|
411
417
|
methods: providerLoginMethods((modelRuntime as any).getProviders?.() ?? []),
|
|
412
418
|
cursor: 0,
|
|
419
|
+
query: "",
|
|
420
|
+
searchFocused: true,
|
|
421
|
+
customVisible: true,
|
|
413
422
|
}));
|
|
414
423
|
const [modelQuery, setModelQuery] = useState("");
|
|
415
424
|
const [modelSearchFocused, setModelSearchFocused] = useState(false);
|
|
@@ -429,6 +438,7 @@ export function App({
|
|
|
429
438
|
? agents.find((agent) => agent.id === activeAgentId)
|
|
430
439
|
: undefined;
|
|
431
440
|
const questionnaire = questionnaireManager?.current();
|
|
441
|
+
const spawnPreview = spawnPreviewManager?.current();
|
|
432
442
|
const visibleTx = activeAgent?.transcript ?? tx;
|
|
433
443
|
const visibleBusy = activeAgent
|
|
434
444
|
? activeAgent.status === "starting" || activeAgent.status === "running"
|
|
@@ -460,6 +470,7 @@ export function App({
|
|
|
460
470
|
|
|
461
471
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
462
472
|
const questionnaireInputRef = useRef<TextareaRenderable>(null);
|
|
473
|
+
const spawnPreviewInputRef = useRef<TextareaRenderable>(null);
|
|
463
474
|
const settingsOpenRef = useRef(settingsOpen);
|
|
464
475
|
const triggersOpenRef = useRef(false);
|
|
465
476
|
const settingsPageRef = useRef(page);
|
|
@@ -480,6 +491,10 @@ export function App({
|
|
|
480
491
|
const imagePasteBusy = useRef(false);
|
|
481
492
|
const viewDrafts = useRef(new Map<string, string>());
|
|
482
493
|
const viewEditingStashIndices = useRef(new Map<string, number | null>());
|
|
494
|
+
const spawnPreviewRestoreView = useRef<{ active: boolean; agentId: string | null }>({
|
|
495
|
+
active: false,
|
|
496
|
+
agentId: null,
|
|
497
|
+
});
|
|
483
498
|
/** Cache row currently checked out into the selected transcript input. */
|
|
484
499
|
const editingStashIndex = useRef<number | null>(null);
|
|
485
500
|
const quitTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
@@ -801,6 +816,10 @@ export function App({
|
|
|
801
816
|
[subagentManager],
|
|
802
817
|
);
|
|
803
818
|
|
|
819
|
+
useEffect(() => spawnPreviewManager?.subscribe(() => {
|
|
820
|
+
setSpawnPreviewRevision((revision) => revision + 1);
|
|
821
|
+
}), [spawnPreviewManager]);
|
|
822
|
+
|
|
804
823
|
useEffect(() => {
|
|
805
824
|
if (!questionnaireManager) return;
|
|
806
825
|
return questionnaireManager.subscribe(() => {
|
|
@@ -937,8 +956,9 @@ export function App({
|
|
|
937
956
|
clearTimeout(cancelTimer.current);
|
|
938
957
|
clearPendingImages();
|
|
939
958
|
cleanupPendingImages();
|
|
959
|
+
spawnPreviewManager?.cancelAll("shutdown");
|
|
940
960
|
},
|
|
941
|
-
[],
|
|
961
|
+
[spawnPreviewManager],
|
|
942
962
|
);
|
|
943
963
|
|
|
944
964
|
useEffect(() => {
|
|
@@ -1161,6 +1181,22 @@ export function App({
|
|
|
1161
1181
|
session.abort().finally(() => setWorking(false));
|
|
1162
1182
|
};
|
|
1163
1183
|
|
|
1184
|
+
const recallQueuedUserMessage = async (target: string | null) => {
|
|
1185
|
+
const recalled = target
|
|
1186
|
+
? await subagentManager.recallQueuedUserMessage(target)
|
|
1187
|
+
: await recallNewestQueuedUserMessage(session, tx.pending);
|
|
1188
|
+
if (!recalled) return;
|
|
1189
|
+
if (!target) dropPending(recalled.id);
|
|
1190
|
+
const key = target ?? "main";
|
|
1191
|
+
viewDrafts.current.set(key, recalled.text);
|
|
1192
|
+
if (activeAgentIdRef.current === target) {
|
|
1193
|
+
setEditorText(recalled.text);
|
|
1194
|
+
histCursor.current = null;
|
|
1195
|
+
draft.current = "";
|
|
1196
|
+
inputRef.current?.focus();
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1164
1200
|
/** Up walks back through sent prompts, down returns to the current draft. */
|
|
1165
1201
|
const recall = (direction: -1 | 1) => {
|
|
1166
1202
|
const input = inputRef.current;
|
|
@@ -1276,6 +1312,7 @@ export function App({
|
|
|
1276
1312
|
promptText: string,
|
|
1277
1313
|
displayText: string,
|
|
1278
1314
|
images: ReturnType<typeof imageContent>[] = [],
|
|
1315
|
+
recallable = images.length === 0,
|
|
1279
1316
|
) => {
|
|
1280
1317
|
const userLine: Extract<Line, { kind: "text" }> = {
|
|
1281
1318
|
kind: "text",
|
|
@@ -1290,6 +1327,7 @@ export function App({
|
|
|
1290
1327
|
id: randomUUID().slice(0, 12),
|
|
1291
1328
|
line: userLine,
|
|
1292
1329
|
deliveryText: promptText,
|
|
1330
|
+
recallable,
|
|
1293
1331
|
};
|
|
1294
1332
|
addPending(pending);
|
|
1295
1333
|
withSearchRoute(session.sessionId, () => session.steer(promptText, images)).catch((error) => {
|
|
@@ -1386,7 +1424,7 @@ export function App({
|
|
|
1386
1424
|
setStash(next);
|
|
1387
1425
|
refreshHistoryAfterStashMutation();
|
|
1388
1426
|
resetAfterCacheExecution();
|
|
1389
|
-
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts));
|
|
1427
|
+
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1390
1428
|
};
|
|
1391
1429
|
|
|
1392
1430
|
useEffect(() => {
|
|
@@ -1411,15 +1449,15 @@ export function App({
|
|
|
1411
1449
|
);
|
|
1412
1450
|
const prompts = entries.map((entry) => entry.text);
|
|
1413
1451
|
if (prompts.length > 1) {
|
|
1414
|
-
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts));
|
|
1452
|
+
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1415
1453
|
return { count: prompts.length, route: "main" };
|
|
1416
1454
|
}
|
|
1417
1455
|
const prompt = prompts[0]!;
|
|
1418
1456
|
if (request.requester.kind === "subagent") {
|
|
1419
|
-
await subagentManager.sendUserMessage(request.requester.id, prompt, [], prompt);
|
|
1457
|
+
await subagentManager.sendUserMessage(request.requester.id, prompt, [], prompt, false);
|
|
1420
1458
|
return { count: 1, route: "subagent" };
|
|
1421
1459
|
}
|
|
1422
|
-
deliverMainPrompt(prompt, prompt);
|
|
1460
|
+
deliverMainPrompt(prompt, prompt, [], false);
|
|
1423
1461
|
return { count: 1, route: "main" };
|
|
1424
1462
|
},
|
|
1425
1463
|
);
|
|
@@ -1436,7 +1474,7 @@ export function App({
|
|
|
1436
1474
|
if (action === "pause") result = triggerManager.pause(trigger.id);
|
|
1437
1475
|
else if (action === "resume") result = triggerManager.resume(trigger.id);
|
|
1438
1476
|
else if (action === "cancel") result = triggerManager.cancel(trigger.id);
|
|
1439
|
-
else result = triggerManager.invoke(trigger.id
|
|
1477
|
+
else result = triggerManager.invoke(trigger.id);
|
|
1440
1478
|
Promise.resolve(result).catch((error) => append({
|
|
1441
1479
|
kind: "text",
|
|
1442
1480
|
role: "error",
|
|
@@ -1550,6 +1588,36 @@ export function App({
|
|
|
1550
1588
|
return true;
|
|
1551
1589
|
};
|
|
1552
1590
|
|
|
1591
|
+
useEffect(() => {
|
|
1592
|
+
if (spawnPreview) {
|
|
1593
|
+
if (!spawnPreviewRestoreView.current.active) {
|
|
1594
|
+
spawnPreviewRestoreView.current = {
|
|
1595
|
+
active: true,
|
|
1596
|
+
agentId: activeAgentIdRef.current,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
settingsOpenRef.current = false;
|
|
1600
|
+
setSettingsOpen(false);
|
|
1601
|
+
setHelpOpen(false);
|
|
1602
|
+
setHistoryOpen(false);
|
|
1603
|
+
setAgentSelectorOpen(false);
|
|
1604
|
+
setTriggerPopup(false, false);
|
|
1605
|
+
setLoginOpen(false);
|
|
1606
|
+
const target = spawnPreview.requester.agentId;
|
|
1607
|
+
if (target && !agents.some((agent) => agent.id === target)) {
|
|
1608
|
+
spawnPreviewManager?.cancel("unavailable");
|
|
1609
|
+
} else if (!selectAgentView(target)) {
|
|
1610
|
+
spawnPreviewManager?.cancel("unavailable");
|
|
1611
|
+
}
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (!spawnPreviewRestoreView.current.active) return;
|
|
1615
|
+
const target = spawnPreviewRestoreView.current.agentId;
|
|
1616
|
+
spawnPreviewRestoreView.current.active = false;
|
|
1617
|
+
if (!target || agents.some((agent) => agent.id === target)) selectAgentView(target);
|
|
1618
|
+
else selectAgentView(null);
|
|
1619
|
+
}, [spawnPreview?.id]);
|
|
1620
|
+
|
|
1553
1621
|
const cycleAgentView = (direction: -1 | 1) => {
|
|
1554
1622
|
const ids: Array<string | null> = [null, ...agents.map((agent) => agent.id)];
|
|
1555
1623
|
if (ids.length === 1) return;
|
|
@@ -1582,11 +1650,27 @@ export function App({
|
|
|
1582
1650
|
return;
|
|
1583
1651
|
}
|
|
1584
1652
|
|
|
1653
|
+
if (spawnPreview) {
|
|
1654
|
+
const isPreviewReturn = ["return", "enter", "kpenter", "linefeed"].includes(key.name);
|
|
1655
|
+
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
1656
|
+
key.stopPropagation();
|
|
1657
|
+
spawnPreviewInputRef.current?.setText("");
|
|
1658
|
+
spawnPreviewManager?.cancel();
|
|
1659
|
+
} else if (isPreviewReturn && !key.shift && !key.ctrl && !key.meta && !key.option) {
|
|
1660
|
+
key.stopPropagation();
|
|
1661
|
+
const note = spawnPreviewInputRef.current?.plainText ?? "";
|
|
1662
|
+
spawnPreviewInputRef.current?.setText("");
|
|
1663
|
+
spawnPreviewManager?.approve(note);
|
|
1664
|
+
}
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1585
1668
|
if (key.ctrl && key.name === "c") {
|
|
1586
1669
|
key.stopPropagation();
|
|
1587
1670
|
resetCancelArm();
|
|
1588
1671
|
const promptOwnsInput =
|
|
1589
1672
|
!questionnaire &&
|
|
1673
|
+
!spawnPreview &&
|
|
1590
1674
|
!loginOpen &&
|
|
1591
1675
|
!triggersOpenRef.current &&
|
|
1592
1676
|
!agentSelectorOpen &&
|
|
@@ -1658,8 +1742,7 @@ export function App({
|
|
|
1658
1742
|
}
|
|
1659
1743
|
|
|
1660
1744
|
if (loginOpen) {
|
|
1661
|
-
key.stopPropagation();
|
|
1662
|
-
loginControllerRef.current?.handleKey(key);
|
|
1745
|
+
if (loginControllerRef.current?.handleKey(key)) key.stopPropagation();
|
|
1663
1746
|
return;
|
|
1664
1747
|
}
|
|
1665
1748
|
|
|
@@ -2015,7 +2098,6 @@ export function App({
|
|
|
2015
2098
|
moveStash(key.name === "up" ? -1 : 1, key.shift);
|
|
2016
2099
|
return;
|
|
2017
2100
|
}
|
|
2018
|
-
if (activeAgentId) return;
|
|
2019
2101
|
if (commandMatches.length > 0) {
|
|
2020
2102
|
key.stopPropagation();
|
|
2021
2103
|
const next = moveCommandSelection(
|
|
@@ -2029,6 +2111,22 @@ export function App({
|
|
|
2029
2111
|
}
|
|
2030
2112
|
// Keep arrow navigation inside multiline or visually wrapped prompts.
|
|
2031
2113
|
if ((inputRef.current?.editorView.getTotalVirtualLineCount() ?? 1) > 1) return;
|
|
2114
|
+
if (key.name === "up" && !inputValue && pendingImages.current.length === 0) {
|
|
2115
|
+
const queued = visibleTx.pending.some((pending) =>
|
|
2116
|
+
!pending.delivered &&
|
|
2117
|
+
pending.line.kind === "text" &&
|
|
2118
|
+
pending.line.role === "user" &&
|
|
2119
|
+
Boolean(pending.deliveryText),
|
|
2120
|
+
);
|
|
2121
|
+
if (queued) {
|
|
2122
|
+
key.stopPropagation();
|
|
2123
|
+
void recallQueuedUserMessage(activeAgentIdRef.current).catch((error) =>
|
|
2124
|
+
append({ kind: "text", role: "error", text: String(error) }),
|
|
2125
|
+
);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
if (activeAgentId) return;
|
|
2032
2130
|
key.stopPropagation();
|
|
2033
2131
|
recall(key.name === "up" ? -1 : 1);
|
|
2034
2132
|
return;
|
|
@@ -2267,7 +2365,7 @@ export function App({
|
|
|
2267
2365
|
selectionBg={theme.selectionBg}
|
|
2268
2366
|
wrapMode="char"
|
|
2269
2367
|
scrollMargin={1}
|
|
2270
|
-
focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !checkApproval}
|
|
2368
|
+
focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !checkApproval}
|
|
2271
2369
|
onContentChange={handleTextareaChange}
|
|
2272
2370
|
onCursorChange={scheduleInputMetrics}
|
|
2273
2371
|
onSubmit={() => submitPrompt()}
|
|
@@ -2287,7 +2385,23 @@ export function App({
|
|
|
2287
2385
|
role="inputBottom"
|
|
2288
2386
|
/>
|
|
2289
2387
|
{loginOpen ? (
|
|
2290
|
-
<LoginPopup
|
|
2388
|
+
<LoginPopup
|
|
2389
|
+
theme={theme}
|
|
2390
|
+
page={loginPage}
|
|
2391
|
+
terminalWidth={width}
|
|
2392
|
+
terminalHeight={height}
|
|
2393
|
+
onProviderSearchChange={(value) => loginControllerRef.current?.setProviderQuery(value)}
|
|
2394
|
+
/>
|
|
2395
|
+
) : null}
|
|
2396
|
+
{spawnPreview ? (
|
|
2397
|
+
<SpawnPreviewPopup
|
|
2398
|
+
theme={theme}
|
|
2399
|
+
syntaxStyle={syntaxStyle}
|
|
2400
|
+
request={spawnPreview}
|
|
2401
|
+
terminalWidth={width}
|
|
2402
|
+
terminalHeight={height}
|
|
2403
|
+
inputRef={spawnPreviewInputRef}
|
|
2404
|
+
/>
|
|
2291
2405
|
) : null}
|
|
2292
2406
|
{questionnaire ? (
|
|
2293
2407
|
<QuestionnairePopup
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export type BrowserLaunchCommand = {
|
|
4
|
+
executable: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type BrowserProcessSpawner = (
|
|
9
|
+
executable: string,
|
|
10
|
+
args: readonly string[],
|
|
11
|
+
) => Promise<void>;
|
|
12
|
+
|
|
13
|
+
export function credentialFreeHttpUrl(value: string): string | null {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(value);
|
|
16
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
17
|
+
if (url.username || url.password) return null;
|
|
18
|
+
return url.href;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function browserLaunchCommand(
|
|
25
|
+
platform: NodeJS.Platform,
|
|
26
|
+
url: string,
|
|
27
|
+
): BrowserLaunchCommand | null {
|
|
28
|
+
if (platform === "win32") {
|
|
29
|
+
return {
|
|
30
|
+
executable: "rundll32.exe",
|
|
31
|
+
args: ["url.dll,FileProtocolHandler", url],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (platform === "darwin") return { executable: "open", args: [url] };
|
|
35
|
+
if (platform === "linux") return { executable: "xdg-open", args: [url] };
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const spawnBrowserProcess: BrowserProcessSpawner = (executable, args) =>
|
|
40
|
+
new Promise<void>((resolve, reject) => {
|
|
41
|
+
const child = spawn(executable, [...args], {
|
|
42
|
+
detached: true,
|
|
43
|
+
stdio: "ignore",
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
});
|
|
46
|
+
child.once("spawn", resolve);
|
|
47
|
+
child.once("error", reject);
|
|
48
|
+
child.unref();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export async function launchBrowserUrl(
|
|
52
|
+
value: string,
|
|
53
|
+
options: {
|
|
54
|
+
platform?: NodeJS.Platform;
|
|
55
|
+
spawn?: BrowserProcessSpawner;
|
|
56
|
+
} = {},
|
|
57
|
+
): Promise<boolean> {
|
|
58
|
+
const url = credentialFreeHttpUrl(value);
|
|
59
|
+
if (!url) return false;
|
|
60
|
+
const command = browserLaunchCommand(options.platform ?? process.platform, url);
|
|
61
|
+
if (!command) return false;
|
|
62
|
+
try {
|
|
63
|
+
await (options.spawn ?? spawnBrowserProcess)(command.executable, command.args);
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "./theme";
|
|
2
|
+
import { PopupFrame } from "./popup-frame";
|
|
2
3
|
|
|
3
4
|
export type CheckApprovalDecision =
|
|
4
5
|
| "allowOnce"
|
|
@@ -149,21 +150,19 @@ export function CheckApprovalPopup(props: CheckApprovalPopupProps) {
|
|
|
149
150
|
const previewLabel = request.preview.kind === "command" ? "Command" : "Diff";
|
|
150
151
|
|
|
151
152
|
return (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
<PopupFrame
|
|
154
|
+
theme={theme}
|
|
155
|
+
terminalWidth={terminalWidth}
|
|
156
|
+
terminalHeight={terminalHeight}
|
|
157
|
+
geometry={{
|
|
156
158
|
top: Math.max(0, Math.floor((terminalHeight - layout.popupHeight) / 2)),
|
|
157
159
|
left: layout.margin,
|
|
158
160
|
width: layout.popupWidth,
|
|
159
161
|
height: layout.popupHeight,
|
|
160
|
-
zIndex: 140,
|
|
161
|
-
border: true,
|
|
162
|
-
borderColor: theme.warn,
|
|
163
|
-
backgroundColor: theme.popupBg,
|
|
164
|
-
flexDirection: "column",
|
|
165
|
-
padding: 1,
|
|
166
162
|
}}
|
|
163
|
+
zIndex={140}
|
|
164
|
+
title=" Approval required "
|
|
165
|
+
borderColor={theme.warn}
|
|
167
166
|
>
|
|
168
167
|
<box style={{ height: 1, flexShrink: 0, flexDirection: "row" }}>
|
|
169
168
|
<text content={request.tool} fg={theme.warn} bg={theme.popupBg} wrapMode="none" />
|
|
@@ -220,6 +219,6 @@ export function CheckApprovalPopup(props: CheckApprovalPopupProps) {
|
|
|
220
219
|
wrapMode="none"
|
|
221
220
|
style={{ height: 1, flexShrink: 0 }}
|
|
222
221
|
/>
|
|
223
|
-
</
|
|
222
|
+
</PopupFrame>
|
|
224
223
|
);
|
|
225
224
|
}
|