pum-agent 0.2.27-beta.2 → 0.2.28-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 +3 -2
- package/package.json +1 -1
- package/src/agent-selector.tsx +6 -3
- package/src/animation.tsx +62 -15
- package/src/app.tsx +29 -4
- package/src/commands.ts +1 -1
- package/src/headless.ts +10 -1
- package/src/help-popup.tsx +5 -5
- package/src/main.tsx +13 -1
- package/src/memory-identity.ts +76 -0
- package/src/memory.ts +327 -0
- package/src/orca-status.ts +80 -0
- package/src/prompt-cache.ts +21 -2
- package/src/replay.ts +12 -1
- package/src/stash-batch.ts +4 -3
- package/src/subagents/manager.ts +104 -34
- package/src/subagents/readonly.ts +1 -0
- package/src/subagents/spawn-preview-popup.tsx +4 -2
- package/src/subagents/types.ts +5 -3
- package/src/tool-groups.ts +10 -3
- package/src/tool-line.ts +5 -1
- package/src/transcript-output.ts +10 -6
- package/src/transcript.tsx +17 -1
- package/src/worktree.ts +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**A compact coding agent for the terminal.**
|
|
6
6
|
|
|
7
|
-
Plan, edit, run commands, review Markdown, and coordinate parallel
|
|
7
|
+
Plan, edit, run commands, review Markdown, and coordinate parallel subagents without leaving the TUI.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/eugen1763/Pum/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/pum-agent)
|
|
@@ -51,8 +51,9 @@ drives the actual TUI and converts the captured cells to SVG.
|
|
|
51
51
|
## What it does
|
|
52
52
|
|
|
53
53
|
- **A full coding loop** — `read`, `write`, `edit`, and `bash`, with streaming Markdown, syntax highlighting, usage, cost, and Git status.
|
|
54
|
-
- **Parallel subagents** — persistent agents
|
|
54
|
+
- **Parallel subagents** — persistent agents that share the project by default, with optional isolated Git worktrees. They message each other durably and report to their spawner. See [Subagents](docs/subagents.md).
|
|
55
55
|
- **Goals that outlive a turn** — `/goal` keeps working, reviewed after each turn by a judge that reads but never writes. See [Goals](docs/goals.md).
|
|
56
|
+
- **Project memory across sessions** — the main agent maintains private Markdown facts that follow linked Git worktrees. See [Tools](docs/tools.md#project-memory).
|
|
56
57
|
- **Supervised processes** — background shells and external triggers such as `gh run watch`, which wake the exact agent that was waiting. See [Tools](docs/tools.md).
|
|
57
58
|
- **Layered safeguards** — a path guard on the file tools, deterministic Check mode, and a native OS sandbox for the processes the model starts. See [Safety](docs/security.md).
|
|
58
59
|
- **A terminal-first look** — nine themes, semantic colour overrides, and optional animation. See [Appearance](docs/appearance.md).
|
package/package.json
CHANGED
package/src/agent-selector.tsx
CHANGED
|
@@ -18,6 +18,7 @@ export type AgentTreeRow = {
|
|
|
18
18
|
name: string;
|
|
19
19
|
status?: SubagentSnapshot["status"];
|
|
20
20
|
readonly?: boolean;
|
|
21
|
+
shared?: boolean;
|
|
21
22
|
depth: number;
|
|
22
23
|
metadata?: StatusMetadataValues;
|
|
23
24
|
};
|
|
@@ -38,7 +39,7 @@ export function agentSelectorRowLayout(
|
|
|
38
39
|
// One column remains clear for the pinned scrollbar.
|
|
39
40
|
const contentColumns = Math.max(1, popupColumns - indent - 1);
|
|
40
41
|
const label = row.status
|
|
41
|
-
? `${row.name}${row.readonly ? " · readonly" : ""} · ${row.status}`
|
|
42
|
+
? `${row.name}${row.readonly ? " · readonly" : ""}${row.shared ? " · shared" : ""} · ${row.status}`
|
|
42
43
|
: row.name;
|
|
43
44
|
const minimumLabelWidth = Math.min(
|
|
44
45
|
label.length,
|
|
@@ -77,9 +78,10 @@ export function buildAgentTree(agents: readonly SubagentSnapshot[]): AgentTreeRo
|
|
|
77
78
|
name: agent.name,
|
|
78
79
|
status: agent.status,
|
|
79
80
|
readonly: agent.readonly === true,
|
|
81
|
+
shared: agent.usesWorktree === false,
|
|
80
82
|
depth,
|
|
81
83
|
metadata: {
|
|
82
|
-
branch: agent.worktree.branch ?? null,
|
|
84
|
+
branch: agent.usesWorktree === false ? null : agent.worktree.branch ?? null,
|
|
83
85
|
outgoingTokens: usage.outgoing,
|
|
84
86
|
incomingTokens: usage.incoming,
|
|
85
87
|
cacheReadTokens: usage.cacheRead,
|
|
@@ -100,9 +102,10 @@ export function buildAgentTree(agents: readonly SubagentSnapshot[]): AgentTreeRo
|
|
|
100
102
|
name: agent.name,
|
|
101
103
|
status: agent.status,
|
|
102
104
|
readonly: agent.readonly === true,
|
|
105
|
+
shared: agent.usesWorktree === false,
|
|
103
106
|
depth: 1,
|
|
104
107
|
metadata: {
|
|
105
|
-
branch: agent.worktree.branch ?? null,
|
|
108
|
+
branch: agent.usesWorktree === false ? null : agent.worktree.branch ?? null,
|
|
106
109
|
outgoingTokens: usage.outgoing,
|
|
107
110
|
incomingTokens: usage.incoming,
|
|
108
111
|
cacheReadTokens: usage.cacheRead,
|
package/src/animation.tsx
CHANGED
|
@@ -143,7 +143,12 @@ const RULE_CHARS_PER_MS = 0.04;
|
|
|
143
143
|
const RULE_HIGHLIGHT_WIDTH = 10;
|
|
144
144
|
const COMET_CHARS_PER_MS = 0.035;
|
|
145
145
|
const ELECTRIC_FRAME_MS = 140;
|
|
146
|
-
|
|
146
|
+
/** Average columns per star. Each slot places its own star inside this span. */
|
|
147
|
+
const CONSTELLATION_SPACING = 11;
|
|
148
|
+
/** Base period of one star. A hashed factor spreads the real periods around it. */
|
|
149
|
+
const CONSTELLATION_TWINKLE_MS = 2600;
|
|
150
|
+
/** How much of its period a star burns for. It is dark and moves in the rest. */
|
|
151
|
+
const CONSTELLATION_LIFETIME = 0.72;
|
|
147
152
|
const RANDOM_CONSTELLATION_CYCLE_MS = 2000;
|
|
148
153
|
/** How much of one cycle a single sparkle burns for, start to finish. */
|
|
149
154
|
const RANDOM_CONSTELLATION_LIFETIME = 0.5;
|
|
@@ -658,6 +663,44 @@ const hashPosition = (frame: number, seed: number, width: number) => {
|
|
|
658
663
|
return Math.abs(value) % width;
|
|
659
664
|
};
|
|
660
665
|
|
|
666
|
+
export type ConstellationStar = { column: number; strength: number };
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* One star of the constellation field, for the slot of columns it belongs to.
|
|
670
|
+
*
|
|
671
|
+
* The field used to be a fixed grid: a star every `CONSTELLATION_SPACING`
|
|
672
|
+
* columns, in the same place for the whole run. Each slot now holds a star with
|
|
673
|
+
* its own period, its own start, its own peak brightness, and a column that is
|
|
674
|
+
* re-drawn for every life. A star is dark between two lives, so it never moves
|
|
675
|
+
* while it is visible. Returns `null` while the slot is empty.
|
|
676
|
+
*/
|
|
677
|
+
export function constellationStar(
|
|
678
|
+
slot: number,
|
|
679
|
+
elapsedMs: number,
|
|
680
|
+
role: WorkingRuleRole,
|
|
681
|
+
): ConstellationStar | null {
|
|
682
|
+
if (slot < 0) return null;
|
|
683
|
+
const seed = roleSeed(role);
|
|
684
|
+
const period = CONSTELLATION_TWINKLE_MS *
|
|
685
|
+
(0.55 + (hashPosition(slot * 13 + 7, seed + 3, 100) / 100) * 1.05);
|
|
686
|
+
const offset = (hashPosition(slot * 29 + 5, seed + 11, 1000) / 1000) * period;
|
|
687
|
+
const time = elapsedMs + offset;
|
|
688
|
+
const life = (time % period) / period;
|
|
689
|
+
if (life >= CONSTELLATION_LIFETIME) return null;
|
|
690
|
+
|
|
691
|
+
const epoch = Math.floor(time / period);
|
|
692
|
+
// Two columns of margin on each side keep the halo of one star clear of the
|
|
693
|
+
// next slot, so no two stars ever touch.
|
|
694
|
+
const span = CONSTELLATION_SPACING - 4;
|
|
695
|
+
const column = slot * CONSTELLATION_SPACING + 2 +
|
|
696
|
+
hashPosition(epoch * 31 + slot, seed * 7 + slot, span);
|
|
697
|
+
const amplitude = 0.5 + (hashPosition(epoch * 17 + slot, seed + 23, 100) / 100) * 0.5;
|
|
698
|
+
const strength = clampStrength(
|
|
699
|
+
Math.sin((life / CONSTELLATION_LIFETIME) * Math.PI) ** 1.2 * amplitude,
|
|
700
|
+
);
|
|
701
|
+
return { column, strength };
|
|
702
|
+
}
|
|
703
|
+
|
|
661
704
|
/** Choose stable, random-looking sparkle centers for one two-second cycle. */
|
|
662
705
|
export function randomConstellationCenters(
|
|
663
706
|
width: number,
|
|
@@ -754,20 +797,24 @@ export function workingRuleCell(
|
|
|
754
797
|
}
|
|
755
798
|
|
|
756
799
|
if (mode === "constellation") {
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
//
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
800
|
+
const slot = Math.floor(column / CONSTELLATION_SPACING);
|
|
801
|
+
let strength = 0;
|
|
802
|
+
let glyph = "─";
|
|
803
|
+
// A star can sit in the neighbouring slot and still reach this column with
|
|
804
|
+
// its halo, which is what makes it bloom instead of switching on in place.
|
|
805
|
+
for (let index = slot - 1; index <= slot + 1; index++) {
|
|
806
|
+
const star = constellationStar(index, elapsedMs, role);
|
|
807
|
+
if (!star || star.column >= width) continue;
|
|
808
|
+
const distance = Math.abs(column - star.column);
|
|
809
|
+
if (distance > 1) continue;
|
|
810
|
+
const lit = distance === 0 ? star.strength : star.strength * CONSTELLATION_HALO;
|
|
811
|
+
if (lit <= strength) continue;
|
|
812
|
+
strength = lit;
|
|
813
|
+
glyph = distance > 0 ? "·"
|
|
814
|
+
: star.strength > 0.82 ? "✦"
|
|
815
|
+
: star.strength > 0.48 ? "✧" : "·";
|
|
816
|
+
}
|
|
817
|
+
return { strength, glyph };
|
|
771
818
|
}
|
|
772
819
|
|
|
773
820
|
if (mode === "random-constellation") {
|
package/src/app.tsx
CHANGED
|
@@ -232,6 +232,7 @@ import {
|
|
|
232
232
|
type ShellManagerLike,
|
|
233
233
|
} from "./processes-popup";
|
|
234
234
|
import type { TerminalTitleController } from "./terminal-title";
|
|
235
|
+
import { resolveOrcaStatusState, type OrcaStatusController } from "./orca-status";
|
|
235
236
|
import { readClipboardText } from "./text-paste";
|
|
236
237
|
import { copyTextToClipboard } from "./clipboard";
|
|
237
238
|
import { NewsPopup } from "./news-popup";
|
|
@@ -831,6 +832,7 @@ export function App({
|
|
|
831
832
|
shellManager,
|
|
832
833
|
messageCacheController,
|
|
833
834
|
terminalTitle,
|
|
835
|
+
orcaStatus,
|
|
834
836
|
startupWarnings = [],
|
|
835
837
|
onSandboxModeChange,
|
|
836
838
|
sandboxWarningSource,
|
|
@@ -870,6 +872,7 @@ export function App({
|
|
|
870
872
|
shellManager?: ShellManagerLike;
|
|
871
873
|
messageCacheController?: MessageCacheController;
|
|
872
874
|
terminalTitle?: TerminalTitleController;
|
|
875
|
+
orcaStatus?: OrcaStatusController;
|
|
873
876
|
/** Visible process-local warnings. These lines never enter pi session context. */
|
|
874
877
|
startupWarnings?: readonly string[];
|
|
875
878
|
onSandboxModeChange?: (mode: NonNullable<PumSettings["sandboxMode"]>) => void;
|
|
@@ -1242,7 +1245,9 @@ export function App({
|
|
|
1242
1245
|
});
|
|
1243
1246
|
const visibleModelId = activeAgent?.modelId.split("/").slice(1).join("/") || modelId;
|
|
1244
1247
|
const visibleThinkingLevel = activeAgent?.thinkingLevel ?? thinkingLevel;
|
|
1245
|
-
const visibleBranch = activeAgent?.
|
|
1248
|
+
const visibleBranch = activeAgent?.usesWorktree === false
|
|
1249
|
+
? branch
|
|
1250
|
+
: activeAgent?.worktree.branch ?? branch;
|
|
1246
1251
|
const visibleElapsedSec = activeAgent ? agentElapsedSec : elapsedSec;
|
|
1247
1252
|
const visibleUsage = activeAgent?.usage ?? usage;
|
|
1248
1253
|
const agentTreeRows = buildAgentTree(agents);
|
|
@@ -1949,11 +1954,28 @@ export function App({
|
|
|
1949
1954
|
useEffect(() => {
|
|
1950
1955
|
// A delegate answering counts as work even though it is not a managed
|
|
1951
1956
|
// agent, or the terminal reads as idle while AFK decides something.
|
|
1957
|
+
orcaStatus?.update({
|
|
1958
|
+
state: resolveOrcaStatusState({
|
|
1959
|
+
working: busy || activeSubagentCount > 0 || afkAnswering,
|
|
1960
|
+
waitingForUser: Boolean(visibleQuestionnaire),
|
|
1961
|
+
}),
|
|
1962
|
+
model: session.agent.state.model?.id,
|
|
1963
|
+
sessionKey: session.sessionId,
|
|
1964
|
+
});
|
|
1952
1965
|
terminalTitle?.update({
|
|
1953
1966
|
working: busy || activeSubagentCount > 0 || afkAnswering,
|
|
1954
1967
|
activeSubagentCount,
|
|
1955
1968
|
});
|
|
1956
|
-
}, [
|
|
1969
|
+
}, [
|
|
1970
|
+
terminalTitle,
|
|
1971
|
+
orcaStatus,
|
|
1972
|
+
busy,
|
|
1973
|
+
activeSubagentCount,
|
|
1974
|
+
afkAnswering,
|
|
1975
|
+
visibleQuestionnaire,
|
|
1976
|
+
session.sessionId,
|
|
1977
|
+
session.agent.state.model?.id,
|
|
1978
|
+
]);
|
|
1957
1979
|
|
|
1958
1980
|
useEffect(() => spawnPreviewManager?.subscribe(() => {
|
|
1959
1981
|
setSpawnPreviewRevision((revision) => revision + 1);
|
|
@@ -4180,7 +4202,10 @@ export function App({
|
|
|
4180
4202
|
appendRequesterLine(requesterAgentId, {
|
|
4181
4203
|
kind: "text",
|
|
4182
4204
|
role: "system",
|
|
4183
|
-
text: `background agent started: ${spawned.name} (${spawned.id})\n
|
|
4205
|
+
text: `background agent started: ${spawned.name} (${spawned.id})\n`
|
|
4206
|
+
+ (spawned.usesWorktree
|
|
4207
|
+
? `${spawned.worktree.branch}\n${spawned.worktree.path}`
|
|
4208
|
+
: `shared directory\n${spawned.worktree.path}`),
|
|
4184
4209
|
});
|
|
4185
4210
|
})().catch((error) => {
|
|
4186
4211
|
appendRequesterLine(requesterAgentId, {
|
|
@@ -4442,7 +4467,7 @@ export function App({
|
|
|
4442
4467
|
};
|
|
4443
4468
|
|
|
4444
4469
|
const cachedBatchDisplay = (prompts: readonly string[]): string => [
|
|
4445
|
-
`Run ${prompts.length} cached tasks with
|
|
4470
|
+
`Run ${prompts.length} cached tasks with subagents:`,
|
|
4446
4471
|
...prompts.map((prompt, index) => `${index + 1}. ${prompt}`),
|
|
4447
4472
|
].join("\n");
|
|
4448
4473
|
|
package/src/commands.ts
CHANGED
package/src/headless.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { mkdirSync } from "node:fs";
|
|
9
9
|
import { AGENT_DIR, AUTH_PATH, MODELS_PATH, sessionDir } from "./config";
|
|
10
|
+
import { createMemoryExtension, MEMORY_EDIT_TOOL_NAME, MEMORY_READ_TOOL_NAME } from "./memory";
|
|
10
11
|
import { checkPathsForProject, loadSettings } from "./settings";
|
|
11
12
|
import { identityExtension } from "./identity";
|
|
12
13
|
import { setWritingStyle, writingStyleExtension } from "./writing-style";
|
|
@@ -34,7 +35,14 @@ import { prepareHeadlessStatsOutput, type HeadlessStatsOutput } from "./headless
|
|
|
34
35
|
* are not constructed here, and subagent, trigger, and message-cache tools
|
|
35
36
|
* need the running TUI for routing and notifications.
|
|
36
37
|
*/
|
|
37
|
-
const HEADLESS_TOOL_NAMES = [
|
|
38
|
+
const HEADLESS_TOOL_NAMES = [
|
|
39
|
+
"read",
|
|
40
|
+
"write",
|
|
41
|
+
"edit",
|
|
42
|
+
"bash",
|
|
43
|
+
MEMORY_READ_TOOL_NAME,
|
|
44
|
+
MEMORY_EDIT_TOOL_NAME,
|
|
45
|
+
];
|
|
38
46
|
|
|
39
47
|
/**
|
|
40
48
|
* Handle one hosted web-search call from a headless run.
|
|
@@ -188,6 +196,7 @@ async function runPromptSession(
|
|
|
188
196
|
checkModePromptExtension,
|
|
189
197
|
checkModeExtension,
|
|
190
198
|
sandboxController.extension(),
|
|
199
|
+
createMemoryExtension({ agentDir: AGENT_DIR, audience: "main" }),
|
|
191
200
|
],
|
|
192
201
|
},
|
|
193
202
|
});
|
package/src/help-popup.tsx
CHANGED
|
@@ -4,14 +4,14 @@ import { PopupFrame } from "./popup-frame";
|
|
|
4
4
|
type HelpGroup = { title: string; controls: [string, string][] };
|
|
5
5
|
|
|
6
6
|
export const HELP_SUMMARY_WIDE = [
|
|
7
|
-
"PUM workflow — prompt or steer · cache prompts · attach images · run managed
|
|
8
|
-
"switch transcripts ·
|
|
7
|
+
"PUM workflow — prompt or steer · cache prompts · attach images · run managed agents in parallel",
|
|
8
|
+
"switch transcripts · close successful agents · optional worktrees · persistent sessions · safety settings",
|
|
9
9
|
] as const;
|
|
10
10
|
|
|
11
11
|
export const HELP_SUMMARY = [
|
|
12
12
|
"Prompt or steer. Cache prompts. Attach images.",
|
|
13
|
-
"Run
|
|
14
|
-
"Switch transcripts and
|
|
13
|
+
"Run agents in parallel. Use worktrees for isolation.",
|
|
14
|
+
"Switch transcripts and close successful agents.",
|
|
15
15
|
"Sessions persist. Settings include safety checks.",
|
|
16
16
|
] as const;
|
|
17
17
|
|
|
@@ -59,7 +59,7 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
59
59
|
["/clear", "Start a fresh session"],
|
|
60
60
|
["/goal", "Set or control a goal"],
|
|
61
61
|
["/goalf", "Work out a goal, then start it"],
|
|
62
|
-
["/background", "Start a
|
|
62
|
+
["/background", "Start a shared-project agent for the selected transcript"],
|
|
63
63
|
["/history", "Browse saved sessions"],
|
|
64
64
|
["/providers", "Add, edit, or delete a provider; /login adds"],
|
|
65
65
|
["/news", "Open recent answers (News)"],
|
package/src/main.tsx
CHANGED
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
ModelRuntime,
|
|
8
8
|
SessionManager,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { mkdirSync } from "node:fs";
|
|
10
|
+
import { mkdirSync, writeSync } from "node:fs";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { App } from "./app";
|
|
13
13
|
import { AGENT_DIR, AUTH_PATH, MODELS_PATH, sessionDir } from "./config";
|
|
14
|
+
import { createMemoryExtension } from "./memory";
|
|
14
15
|
import { checkPathsForProject, loadSettings } from "./settings";
|
|
15
16
|
import { setBashOutputSettingsIfPresent } from "./bash-output";
|
|
16
17
|
import { installWebSearch, webSearch } from "./web-search";
|
|
@@ -49,6 +50,7 @@ import type { StartupOptions } from "./cli";
|
|
|
49
50
|
import { TodoToolsController } from "./todo-tools";
|
|
50
51
|
import { installSelectionClipboard } from "./clipboard";
|
|
51
52
|
import { TerminalTitleController } from "./terminal-title";
|
|
53
|
+
import { isOrcaTerminal, OrcaStatusController } from "./orca-status";
|
|
52
54
|
import { SandboxController } from "./sandbox";
|
|
53
55
|
import {
|
|
54
56
|
createFilesystemSandboxExtension,
|
|
@@ -260,6 +262,9 @@ export async function start(
|
|
|
260
262
|
(_agentId, isReadonly) => sandboxController.extension({ readonly: isReadonly }),
|
|
261
263
|
(_agentId, isReadonly) => createFilesystemSandboxExtension({ readonly: isReadonly }),
|
|
262
264
|
],
|
|
265
|
+
childWorkerExtensionFactories: [
|
|
266
|
+
() => createMemoryExtension({ agentDir: AGENT_DIR, audience: "subagent" }),
|
|
267
|
+
],
|
|
263
268
|
sandboxModeSource: () => sandboxController.mode,
|
|
264
269
|
});
|
|
265
270
|
const subagentExtension = subagentManager.mainExtension();
|
|
@@ -295,6 +300,7 @@ export async function start(
|
|
|
295
300
|
filesystemSandboxExtension,
|
|
296
301
|
mainCheckModeExtension,
|
|
297
302
|
sandboxExtension,
|
|
303
|
+
createMemoryExtension({ agentDir: AGENT_DIR, audience: "main" }),
|
|
298
304
|
questionnaireManager.extension({ id: "main", name: "main" }),
|
|
299
305
|
mainToolGroups.extension(),
|
|
300
306
|
mainTodoTools.extension(),
|
|
@@ -342,6 +348,11 @@ export async function start(
|
|
|
342
348
|
|
|
343
349
|
const renderer = await createCliRenderer({ exitOnCtrlC: false });
|
|
344
350
|
const terminalTitle = new TerminalTitleController((title) => renderer.setTerminalTitle(title));
|
|
351
|
+
const orcaStatus = new OrcaStatusController(
|
|
352
|
+
isOrcaTerminal(process.env),
|
|
353
|
+
// Keep the control message outside OpenTUI's external-output path.
|
|
354
|
+
(sequence) => { writeSync(process.stdout.fd, sequence); },
|
|
355
|
+
);
|
|
345
356
|
const selectionClipboard = installSelectionClipboard(renderer);
|
|
346
357
|
const root = createRoot(renderer);
|
|
347
358
|
// Reported on the restored terminal by the exit action below, so the message
|
|
@@ -445,6 +456,7 @@ export async function start(
|
|
|
445
456
|
spawnPreviewManager={spawnPreviewManager}
|
|
446
457
|
messageCacheController={messageCacheController}
|
|
447
458
|
terminalTitle={terminalTitle}
|
|
459
|
+
orcaStatus={orcaStatus}
|
|
448
460
|
startupWarnings={[
|
|
449
461
|
...(sandboxWarning ? [sandboxWarning] : []),
|
|
450
462
|
...(outerSandbox ? [
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
canonicalRealpathSync,
|
|
6
|
+
projectStorageKey,
|
|
7
|
+
type RuntimePlatform,
|
|
8
|
+
} from "./platform";
|
|
9
|
+
|
|
10
|
+
export type MemoryIdentity = {
|
|
11
|
+
key: string;
|
|
12
|
+
digest: string;
|
|
13
|
+
kind: "git" | "directory";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type MemoryIdentityOptions = {
|
|
17
|
+
platform?: RuntimePlatform;
|
|
18
|
+
gitPath?: (cwd: string, args: string[]) => string;
|
|
19
|
+
realpath?: (path: string, platform: RuntimePlatform) => string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const resolvedIdentities = new Map<string, MemoryIdentity>();
|
|
23
|
+
|
|
24
|
+
function defaultGitPath(cwd: string, args: string[]): string {
|
|
25
|
+
return execFileSync("git", args, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
31
|
+
}).replace(/\r?\n$/, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function digest(key: string): string {
|
|
35
|
+
return createHash("sha256").update(key).digest("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Use Git's shared administrative directory as the repository identity.
|
|
40
|
+
* Every linked worktree reports the same directory, even when its checkout
|
|
41
|
+
* path is unrelated to the primary checkout path.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveMemoryIdentity(
|
|
44
|
+
cwd: string,
|
|
45
|
+
options: MemoryIdentityOptions = {},
|
|
46
|
+
): MemoryIdentity {
|
|
47
|
+
const platform = options.platform ?? process.platform;
|
|
48
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
49
|
+
const realpath = options.realpath ?? canonicalRealpathSync;
|
|
50
|
+
const gitPath = options.gitPath ?? defaultGitPath;
|
|
51
|
+
const cacheKey = `${platform}\0${projectStorageKey(cwd, platform)}`;
|
|
52
|
+
const cacheable = options.gitPath === undefined && options.realpath === undefined;
|
|
53
|
+
if (cacheable) {
|
|
54
|
+
const cached = resolvedIdentities.get(cacheKey);
|
|
55
|
+
if (cached) return cached;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const raw = gitPath(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
60
|
+
const absolute = paths.isAbsolute(raw) ? raw : paths.resolve(cwd, raw);
|
|
61
|
+
const canonical = realpath(absolute, platform);
|
|
62
|
+
const key = `git:${projectStorageKey(canonical, platform)}`;
|
|
63
|
+
const identity: MemoryIdentity = { key, digest: digest(key), kind: "git" };
|
|
64
|
+
if (cacheable) resolvedIdentities.set(cacheKey, identity);
|
|
65
|
+
return identity;
|
|
66
|
+
} catch {
|
|
67
|
+
let canonical = paths.resolve(cwd);
|
|
68
|
+
try {
|
|
69
|
+
canonical = realpath(canonical, platform);
|
|
70
|
+
} catch { /* the session startup owns directory validation */ }
|
|
71
|
+
const key = `directory:${projectStorageKey(canonical, platform)}`;
|
|
72
|
+
const identity: MemoryIdentity = { key, digest: digest(key), kind: "directory" };
|
|
73
|
+
if (cacheable) resolvedIdentities.set(cacheKey, identity);
|
|
74
|
+
return identity;
|
|
75
|
+
}
|
|
76
|
+
}
|