pum-agent 0.2.13-beta.1 → 0.2.14-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/package.json +1 -1
- package/src/app.tsx +0 -1
- package/src/subagents/fork-session.ts +80 -0
- package/src/subagents/manager.ts +103 -34
- package/src/subagents/spawn-preview-popup.tsx +14 -7
- package/src/subagents/types.ts +21 -0
- package/src/tool-line.ts +1 -1
- package/src/transcript.tsx +52 -16
- package/src/writing-style.ts +9 -6
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
CURRENT_SESSION_VERSION,
|
|
4
|
+
SessionManager,
|
|
5
|
+
type SessionEntry,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { ForkOrigin, ForkSource } from "./types";
|
|
8
|
+
|
|
9
|
+
type ForkSessionSource = Pick<
|
|
10
|
+
SessionManager,
|
|
11
|
+
"getSessionId" | "getSessionFile" | "getLeafId" | "getBranch" | "buildContextEntries"
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export function captureForkSource(
|
|
15
|
+
sessionManager: ForkSessionSource,
|
|
16
|
+
sourceAgentId: string | null,
|
|
17
|
+
): ForkSource {
|
|
18
|
+
const cutoffEntryId = sessionManager.getLeafId();
|
|
19
|
+
const entries = cutoffEntryId === null ? [] : sessionManager.getBranch(cutoffEntryId);
|
|
20
|
+
if (cutoffEntryId !== null && entries.at(-1)?.id !== cutoffEntryId) {
|
|
21
|
+
throw new Error(`Cannot fork: cutoff entry ${cutoffEntryId} is not on the active branch`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
origin: {
|
|
25
|
+
sourceSessionId: sessionManager.getSessionId(),
|
|
26
|
+
cutoffEntryId,
|
|
27
|
+
sourceAgentId,
|
|
28
|
+
},
|
|
29
|
+
sourceSessionFile: sessionManager.getSessionFile(),
|
|
30
|
+
entries: structuredClone(entries) as SessionEntry[],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createForkedSession(
|
|
35
|
+
source: ForkSource,
|
|
36
|
+
targetCwd: string,
|
|
37
|
+
sessionDir: string,
|
|
38
|
+
): SessionManager {
|
|
39
|
+
const cutoff = source.origin.cutoffEntryId;
|
|
40
|
+
if ((cutoff === null && source.entries.length !== 0)
|
|
41
|
+
|| (cutoff !== null && source.entries.at(-1)?.id !== cutoff)) {
|
|
42
|
+
throw new Error("Cannot fork: captured active branch does not match the cutoff entry");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const target = SessionManager.create(targetCwd, sessionDir, {
|
|
46
|
+
parentSession: source.sourceSessionFile,
|
|
47
|
+
});
|
|
48
|
+
const sessionFile = target.getSessionFile();
|
|
49
|
+
const header = target.getHeader();
|
|
50
|
+
if (!sessionFile || !header) throw new Error("Cannot fork: target session could not be allocated");
|
|
51
|
+
|
|
52
|
+
const forkHeader = {
|
|
53
|
+
...header,
|
|
54
|
+
version: CURRENT_SESSION_VERSION,
|
|
55
|
+
cwd: targetCwd,
|
|
56
|
+
parentSession: source.sourceSessionFile,
|
|
57
|
+
};
|
|
58
|
+
const content = [forkHeader, ...source.entries]
|
|
59
|
+
.map((entry) => JSON.stringify(entry))
|
|
60
|
+
.join("\n") + "\n";
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(sessionFile, content, { flag: "wx" });
|
|
63
|
+
return SessionManager.open(sessionFile, sessionDir, targetCwd);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
rmSync(sessionFile, { force: true });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function entriesAfterForkCutoff(
|
|
71
|
+
sessionManager: ForkSessionSource,
|
|
72
|
+
origin?: ForkOrigin,
|
|
73
|
+
): SessionEntry[] {
|
|
74
|
+
if (!origin) return sessionManager.buildContextEntries();
|
|
75
|
+
const branch = sessionManager.getBranch();
|
|
76
|
+
if (origin.cutoffEntryId === null) return branch;
|
|
77
|
+
const cutoffIndex = branch.findIndex((entry) => entry.id === origin.cutoffEntryId);
|
|
78
|
+
if (cutoffIndex < 0) return [];
|
|
79
|
+
return branch.slice(cutoffIndex + 1);
|
|
80
|
+
}
|
package/src/subagents/manager.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ModelRuntime,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Type } from "typebox";
|
|
12
|
-
import { existsSync } from "node:fs";
|
|
12
|
+
import { existsSync, rmSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import {
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
type AgentMessageData,
|
|
81
81
|
type TriggerEventData,
|
|
82
82
|
type AgentTranscript,
|
|
83
|
+
type ForkSource,
|
|
83
84
|
type SpawnSubagentOptions,
|
|
84
85
|
type SubagentManagerEvent,
|
|
85
86
|
type SubagentRegistryEvent,
|
|
@@ -90,6 +91,7 @@ import {
|
|
|
90
91
|
import type { SpawnPreviewManager, SpawnPreviewRequester } from "./spawn-preview";
|
|
91
92
|
import { readonlySubagentExtension } from "./readonly";
|
|
92
93
|
import type { SessionStatsManager } from "../session-stats";
|
|
94
|
+
import { captureForkSource, createForkedSession, entriesAfterForkCutoff } from "./fork-session";
|
|
93
95
|
|
|
94
96
|
const MAX_RETAINED_AGENTS = 100;
|
|
95
97
|
const MAX_MESSAGE_LENGTH = 12_000;
|
|
@@ -277,6 +279,12 @@ export function spawnSubagentParameters(readonlyAvailable: boolean) {
|
|
|
277
279
|
task: Type.String({ description: "Complete task for the subagent" }),
|
|
278
280
|
name: Type.Optional(Type.String({ description: "Optional worktree and agent name" })),
|
|
279
281
|
preview: Type.Optional(Type.Boolean({ description: "Ask the user to approve before spawning" })),
|
|
282
|
+
context: Type.Optional(Type.Union([
|
|
283
|
+
Type.Literal("fresh"),
|
|
284
|
+
Type.Literal("fork"),
|
|
285
|
+
], {
|
|
286
|
+
description: 'Conversation context mode. Defaults to "fresh". Use "fork" to inherit the immediate requester conversation before this assistant turn.',
|
|
287
|
+
})),
|
|
280
288
|
...(readonlyAvailable ? { readonly: readonlySpawnParameter() } : {}),
|
|
281
289
|
}, { additionalProperties: false });
|
|
282
290
|
}
|
|
@@ -443,7 +451,11 @@ export class SubagentManager {
|
|
|
443
451
|
);
|
|
444
452
|
restoredEntries.set(snapshot.id, childManager.getEntries());
|
|
445
453
|
transcript = {
|
|
446
|
-
lines: replayEntries(
|
|
454
|
+
lines: replayEntries(
|
|
455
|
+
entriesAfterForkCutoff(childManager, snapshot.forkOrigin),
|
|
456
|
+
snapshot.worktree.path,
|
|
457
|
+
true,
|
|
458
|
+
),
|
|
447
459
|
stream: null,
|
|
448
460
|
pending: [],
|
|
449
461
|
};
|
|
@@ -845,7 +857,6 @@ export class SubagentManager {
|
|
|
845
857
|
: event.toolName.startsWith("message_cache_")
|
|
846
858
|
? messageCacheDetail(event.result)
|
|
847
859
|
: undefined,
|
|
848
|
-
output: undefined,
|
|
849
860
|
exitCode: bashResult.exitCode,
|
|
850
861
|
});
|
|
851
862
|
break;
|
|
@@ -1050,7 +1061,14 @@ export class SubagentManager {
|
|
|
1050
1061
|
thinkingLevel: parent.snapshot.thinkingLevel,
|
|
1051
1062
|
readonly: readonlyRequested,
|
|
1052
1063
|
parentAgentId: agentId,
|
|
1064
|
+
context: params.context ?? "fresh",
|
|
1053
1065
|
};
|
|
1066
|
+
if (options.context === "fork") {
|
|
1067
|
+
options.forkSource = captureForkSource(
|
|
1068
|
+
ctx.sessionManager,
|
|
1069
|
+
agentId,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1054
1072
|
if (params.preview) {
|
|
1055
1073
|
const preview = await this.requestSpawnPreview({
|
|
1056
1074
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -1269,7 +1287,11 @@ export class SubagentManager {
|
|
|
1269
1287
|
modelId: `${ctx.model.provider}/${ctx.model.id}`,
|
|
1270
1288
|
thinkingLevel: ctx.thinkingLevel ?? "off",
|
|
1271
1289
|
readonly: readonlyRequested,
|
|
1290
|
+
context: params.context ?? "fresh",
|
|
1272
1291
|
};
|
|
1292
|
+
if (options.context === "fork") {
|
|
1293
|
+
options.forkSource = captureForkSource(ctx.sessionManager, null);
|
|
1294
|
+
}
|
|
1273
1295
|
if (params.preview) {
|
|
1274
1296
|
const preview = await this.requestSpawnPreview({
|
|
1275
1297
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -1421,6 +1443,11 @@ export class SubagentManager {
|
|
|
1421
1443
|
if (options.readonly === true && this.sandboxModeSource() === "off") {
|
|
1422
1444
|
throw new Error("Readonly subagents require the PUM Sandbox setting to be Auto or Require");
|
|
1423
1445
|
}
|
|
1446
|
+
const context = options.context ?? "fresh";
|
|
1447
|
+
const forkSource = context === "fork"
|
|
1448
|
+
? options.forkSource ?? this.captureSpawnerForkSource(options.parentAgentId ?? null)
|
|
1449
|
+
: undefined;
|
|
1450
|
+
let allocatedSessionFile: string | undefined;
|
|
1424
1451
|
const record = await this.withWorktreeLock(async () => {
|
|
1425
1452
|
if (this.activeCount() >= this.maxActiveSubagents) throw activeLimitError(this.maxActiveSubagents);
|
|
1426
1453
|
if (this.records.size >= MAX_RETAINED_AGENTS) throw new Error(`At most ${MAX_RETAINED_AGENTS} subagents can be retained`);
|
|
@@ -1431,33 +1458,48 @@ export class SubagentManager {
|
|
|
1431
1458
|
}
|
|
1432
1459
|
|
|
1433
1460
|
const worktree = await createWorktree(this.mainCwd, options.name);
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
+
try {
|
|
1462
|
+
const id = randomUUID().slice(0, 8);
|
|
1463
|
+
const now = Date.now();
|
|
1464
|
+
const snapshot: SubagentSnapshot = {
|
|
1465
|
+
id,
|
|
1466
|
+
name: worktree.name,
|
|
1467
|
+
task: options.task,
|
|
1468
|
+
status: "starting",
|
|
1469
|
+
worktree,
|
|
1470
|
+
parentAgentId: options.parentAgentId ?? null,
|
|
1471
|
+
modelId: options.modelId,
|
|
1472
|
+
thinkingLevel: options.thinkingLevel,
|
|
1473
|
+
readonly: options.readonly === true,
|
|
1474
|
+
forkOrigin: forkSource?.origin,
|
|
1475
|
+
transcript: emptyTranscript(),
|
|
1476
|
+
startedAt: now,
|
|
1477
|
+
updatedAt: now,
|
|
1478
|
+
usage: emptyAgentUsage(),
|
|
1479
|
+
};
|
|
1480
|
+
if (forkSource) {
|
|
1481
|
+
const sessionDir = join(this.agentDir, "subagents", this.parentSessionId);
|
|
1482
|
+
allocatedSessionFile = createForkedSession(
|
|
1483
|
+
forkSource,
|
|
1484
|
+
worktree.path,
|
|
1485
|
+
sessionDir,
|
|
1486
|
+
).getSessionFile();
|
|
1487
|
+
snapshot.sessionFile = allocatedSessionFile;
|
|
1488
|
+
}
|
|
1489
|
+
const created: RuntimeRecord = {
|
|
1490
|
+
snapshot,
|
|
1491
|
+
userInstructionNotices: new Map(),
|
|
1492
|
+
activityGeneration: 0,
|
|
1493
|
+
idleNotifiedGeneration: 0,
|
|
1494
|
+
};
|
|
1495
|
+
this.records.set(id, created);
|
|
1496
|
+
this.emit();
|
|
1497
|
+
return created;
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (allocatedSessionFile) rmSync(allocatedSessionFile, { force: true });
|
|
1500
|
+
await removeWorktree(this.mainCwd, worktree).catch(() => {});
|
|
1501
|
+
throw error;
|
|
1502
|
+
}
|
|
1461
1503
|
});
|
|
1462
1504
|
|
|
1463
1505
|
try {
|
|
@@ -1470,11 +1512,30 @@ export class SubagentManager {
|
|
|
1470
1512
|
});
|
|
1471
1513
|
return cloneSnapshot(record);
|
|
1472
1514
|
} catch (error) {
|
|
1473
|
-
|
|
1515
|
+
if (context === "fork") {
|
|
1516
|
+
await record.dispose?.();
|
|
1517
|
+
this.records.delete(record.snapshot.id);
|
|
1518
|
+
if (record.snapshot.sessionFile) rmSync(record.snapshot.sessionFile, { force: true });
|
|
1519
|
+
await this.withWorktreeLock(() => removeWorktree(this.mainCwd, record.snapshot.worktree)).catch(() => {});
|
|
1520
|
+
this.persist({ event: "removed", id: record.snapshot.id, at: Date.now() });
|
|
1521
|
+
this.emit();
|
|
1522
|
+
} else {
|
|
1523
|
+
this.updateStatus(record, "failed", String(error));
|
|
1524
|
+
}
|
|
1474
1525
|
throw error;
|
|
1475
1526
|
}
|
|
1476
1527
|
}
|
|
1477
1528
|
|
|
1529
|
+
private captureSpawnerForkSource(parentAgentId: string | null): ForkSource {
|
|
1530
|
+
if (parentAgentId === null) {
|
|
1531
|
+
if (!this.mainSessionManager) throw new Error("Cannot fork: the main source session is unavailable");
|
|
1532
|
+
return captureForkSource(this.mainSessionManager, null);
|
|
1533
|
+
}
|
|
1534
|
+
const parent = this.records.get(parentAgentId);
|
|
1535
|
+
if (!parent?.session) throw new Error("Cannot fork: the immediate parent session is unavailable");
|
|
1536
|
+
return captureForkSource(parent.session.sessionManager, parentAgentId);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1478
1539
|
private async ensureRuntime(record: RuntimeRecord, retrySettlements = true): Promise<void> {
|
|
1479
1540
|
if (record.session) {
|
|
1480
1541
|
if (retrySettlements) await this.retrySettlementsForParent(record.snapshot.id);
|
|
@@ -2247,9 +2308,17 @@ export class SubagentManager {
|
|
|
2247
2308
|
private formatAgentList(): string {
|
|
2248
2309
|
const agents = this.getAgents();
|
|
2249
2310
|
if (!agents.length) return "No subagents.";
|
|
2250
|
-
return agents.map((agent) =>
|
|
2251
|
-
|
|
2252
|
-
|
|
2311
|
+
return agents.map((agent) => {
|
|
2312
|
+
const origin = agent.forkOrigin;
|
|
2313
|
+
const source = origin?.sourceAgentId
|
|
2314
|
+
? this.records.get(origin.sourceAgentId)?.snapshot.name ?? origin.sourceAgentId
|
|
2315
|
+
: "main";
|
|
2316
|
+
return `${agent.id} ${agent.name} ${agent.status}${agent.readonly ? " readonly" : ""}`
|
|
2317
|
+
+ `\n ${agent.worktree.branch}\n ${agent.worktree.path}`
|
|
2318
|
+
+ (origin
|
|
2319
|
+
? `\n fork source: ${source} · session ${origin.sourceSessionId} · cutoff ${origin.cutoffEntryId ?? "root"}`
|
|
2320
|
+
: "");
|
|
2321
|
+
}).join("\n");
|
|
2253
2322
|
}
|
|
2254
2323
|
|
|
2255
2324
|
async stop(id: string, status: SubagentStatus = "stopped", persist = true): Promise<void> {
|
|
@@ -38,7 +38,11 @@ export function SpawnPreviewPopup({
|
|
|
38
38
|
const geometry = spawnPreviewPopupGeometry(terminalWidth, terminalHeight);
|
|
39
39
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
40
40
|
const noteHeight = geometry.compact ? 1 : Math.min(4, Math.max(1, terminalHeight - 8));
|
|
41
|
-
const taskAreaHeight = Math.max(1, geometry.height - noteHeight - (geometry.compact ? 1 :
|
|
41
|
+
const taskAreaHeight = Math.max(1, geometry.height - noteHeight - (geometry.compact ? 1 : 8));
|
|
42
|
+
const contextMode = request.options.context ?? "fresh";
|
|
43
|
+
const source = contextMode === "fork"
|
|
44
|
+
? `${request.requester.name} · session ${request.requester.sessionId}`
|
|
45
|
+
: "none";
|
|
42
46
|
|
|
43
47
|
useEffect(() => {
|
|
44
48
|
inputRef.current?.setText("");
|
|
@@ -64,12 +68,15 @@ export function SpawnPreviewPopup({
|
|
|
64
68
|
padding={geometry.compact ? 0 : 1}
|
|
65
69
|
>
|
|
66
70
|
{!geometry.compact ? (
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
<box style={{ flexDirection: "column", height: 3, flexShrink: 0 }}>
|
|
72
|
+
<text
|
|
73
|
+
content={request.options.readonly ? "Child task · readonly" : "Child task"}
|
|
74
|
+
fg={theme.accent}
|
|
75
|
+
bg={theme.popupBg}
|
|
76
|
+
/>
|
|
77
|
+
<text content={`Context · ${contextMode}`} fg={theme.dim} bg={theme.popupBg} />
|
|
78
|
+
<text content={`Source · ${source}`} fg={theme.dim} bg={theme.popupBg} />
|
|
79
|
+
</box>
|
|
73
80
|
) : null}
|
|
74
81
|
<scrollbox
|
|
75
82
|
ref={scrollRef}
|
package/src/subagents/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ImageContent } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { Line, PendingLine } from "../transcript";
|
|
3
3
|
import type { WorktreeRecord } from "../worktree";
|
|
4
4
|
import type { AgentUsage } from "../agent-usage";
|
|
5
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import {
|
|
6
7
|
EXTERNAL_TRIGGER_CUSTOM_TYPE,
|
|
7
8
|
type ExternalTriggerEventData,
|
|
@@ -32,6 +33,21 @@ export type AgentTranscript = {
|
|
|
32
33
|
pending: PendingLine[];
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
export type SpawnContextMode = "fresh" | "fork";
|
|
37
|
+
|
|
38
|
+
export type ForkOrigin = {
|
|
39
|
+
sourceSessionId: string;
|
|
40
|
+
cutoffEntryId: string | null;
|
|
41
|
+
sourceAgentId: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Immutable runtime capture. Only origin is persisted in the subagent snapshot. */
|
|
45
|
+
export type ForkSource = {
|
|
46
|
+
origin: ForkOrigin;
|
|
47
|
+
sourceSessionFile?: string;
|
|
48
|
+
entries: readonly SessionEntry[];
|
|
49
|
+
};
|
|
50
|
+
|
|
35
51
|
export type SubagentSnapshot = {
|
|
36
52
|
id: string;
|
|
37
53
|
name: string;
|
|
@@ -45,6 +61,8 @@ export type SubagentSnapshot = {
|
|
|
45
61
|
thinkingLevel: string;
|
|
46
62
|
/** True when the child must not mutate files or delegate filesystem mutation. Missing legacy values mean false. */
|
|
47
63
|
readonly?: boolean;
|
|
64
|
+
/** Present only when this child inherited an exact requester conversation branch. */
|
|
65
|
+
forkOrigin?: ForkOrigin;
|
|
48
66
|
transcript: AgentTranscript;
|
|
49
67
|
summary?: string;
|
|
50
68
|
startedAt: number;
|
|
@@ -121,6 +139,9 @@ export type SpawnSubagentOptions = {
|
|
|
121
139
|
readonly?: boolean;
|
|
122
140
|
createWorktree?: boolean;
|
|
123
141
|
parentAgentId?: string | null;
|
|
142
|
+
context?: SpawnContextMode;
|
|
143
|
+
/** Runtime-only immutable branch capture. This value is not persisted. */
|
|
144
|
+
forkSource?: ForkSource;
|
|
124
145
|
};
|
|
125
146
|
|
|
126
147
|
export type RoutedPrompt = {
|
package/src/tool-line.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type ToolCall = {
|
|
|
9
9
|
state: "running" | "ok" | "error" | "rejected";
|
|
10
10
|
/** "+3 −1" for edits, or an error note. */
|
|
11
11
|
detail?: string;
|
|
12
|
-
/** Cumulative live output
|
|
12
|
+
/** Cumulative live output retained until the Bash output display period ends. */
|
|
13
13
|
output?: string;
|
|
14
14
|
/** Start time used to delay live Bash output without delaying the tool row. */
|
|
15
15
|
startedAt?: number;
|
package/src/transcript.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type SyntaxStyle,
|
|
7
7
|
} from "@opentui/core";
|
|
8
8
|
import type { MarkdownProps } from "@opentui/react";
|
|
9
|
-
import { useEffect, useState } from "react";
|
|
9
|
+
import { useEffect, useRef, useState } from "react";
|
|
10
10
|
import {
|
|
11
11
|
useBlinkingText,
|
|
12
12
|
useMarkdownCaret,
|
|
@@ -374,26 +374,62 @@ export function toolStateGlyph(state: ToolCall["state"]): string {
|
|
|
374
374
|
|
|
375
375
|
const CHECK_MODE_HARD_BLOCK_PREFIX = "Check mode hard block:";
|
|
376
376
|
const BASH_OUTPUT_DELAY_MS = 500;
|
|
377
|
+
const BASH_OUTPUT_MIN_VISIBLE_MS = 2_000;
|
|
378
|
+
|
|
379
|
+
function useBashOutputVisible(call: ToolCall): boolean {
|
|
380
|
+
const initiallyVisible = call.name === "bash"
|
|
381
|
+
&& call.state === "running"
|
|
382
|
+
&& Boolean(call.output)
|
|
383
|
+
&& (call.startedAt === undefined || Date.now() - call.startedAt >= BASH_OUTPUT_DELAY_MS);
|
|
384
|
+
const [visible, setVisible] = useState(initiallyVisible);
|
|
385
|
+
const visibleSince = useRef<number | undefined>(initiallyVisible ? Date.now() : undefined);
|
|
377
386
|
|
|
378
|
-
function useLiveBashOutputReady(call: ToolCall): boolean {
|
|
379
|
-
const [ready, setReady] = useState(() => (
|
|
380
|
-
call.startedAt === undefined || Date.now() - call.startedAt >= BASH_OUTPUT_DELAY_MS
|
|
381
|
-
));
|
|
382
387
|
useEffect(() => {
|
|
383
|
-
if (call.name !== "bash" || call.
|
|
384
|
-
|
|
388
|
+
if (call.name !== "bash" || !call.output) {
|
|
389
|
+
visibleSince.current = undefined;
|
|
390
|
+
setVisible(false);
|
|
385
391
|
return;
|
|
386
392
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
393
|
+
|
|
394
|
+
const show = () => {
|
|
395
|
+
visibleSince.current ??= Date.now();
|
|
396
|
+
setVisible(true);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
if (call.state === "running") {
|
|
400
|
+
if (call.startedAt === undefined) {
|
|
401
|
+
show();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const delay = BASH_OUTPUT_DELAY_MS - (Date.now() - call.startedAt);
|
|
405
|
+
if (delay <= 0) {
|
|
406
|
+
show();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
visibleSince.current = undefined;
|
|
410
|
+
setVisible(false);
|
|
411
|
+
const timer = setTimeout(show, delay);
|
|
412
|
+
return () => clearTimeout(timer);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (visibleSince.current === undefined) {
|
|
416
|
+
if (call.startedAt === undefined || Date.now() - call.startedAt < BASH_OUTPUT_DELAY_MS) {
|
|
417
|
+
setVisible(false);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
show();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const remaining = BASH_OUTPUT_MIN_VISIBLE_MS - (Date.now() - visibleSince.current!);
|
|
424
|
+
if (remaining <= 0) {
|
|
425
|
+
setVisible(false);
|
|
390
426
|
return;
|
|
391
427
|
}
|
|
392
|
-
|
|
393
|
-
const timer = setTimeout(() => setReady(true), delay);
|
|
428
|
+
const timer = setTimeout(() => setVisible(false), remaining);
|
|
394
429
|
return () => clearTimeout(timer);
|
|
395
|
-
}, [call.id, call.name, call.
|
|
396
|
-
|
|
430
|
+
}, [call.id, call.name, call.output, call.startedAt, call.state]);
|
|
431
|
+
|
|
432
|
+
return visible;
|
|
397
433
|
}
|
|
398
434
|
|
|
399
435
|
function rejectedDetail(theme: Theme, detail: string): StyledText {
|
|
@@ -421,7 +457,7 @@ export function ToolLine({
|
|
|
421
457
|
const spinner = useSpinner(call.state === "running");
|
|
422
458
|
const failed = call.state === "error";
|
|
423
459
|
const rejected = call.state === "rejected";
|
|
424
|
-
const
|
|
460
|
+
const bashOutputVisible = useBashOutputVisible(call);
|
|
425
461
|
const toolColor = failed ? theme.error : rejected ? theme.rejection : theme.tool;
|
|
426
462
|
const argColor = failed ? theme.error : rejected ? theme.rejection : theme.toolArg;
|
|
427
463
|
const detailColor = failed ? theme.error : rejected ? theme.rejection : theme.dim;
|
|
@@ -443,7 +479,7 @@ export function ToolLine({
|
|
|
443
479
|
caretColor: failed ? theme.error : rejected ? theme.rejection : theme.accent,
|
|
444
480
|
active: workingCaret,
|
|
445
481
|
});
|
|
446
|
-
const output = call.name === "bash" && call.
|
|
482
|
+
const output = call.name === "bash" && call.output && bashOutputVisible
|
|
447
483
|
? bashOutputWindow(call.output)
|
|
448
484
|
: null;
|
|
449
485
|
|
package/src/writing-style.ts
CHANGED
|
@@ -9,23 +9,26 @@ let currentStyle: WritingStyle = "none";
|
|
|
9
9
|
* Practical ASD-STE100 guidance for model output. The controlled dictionary is
|
|
10
10
|
* not embedded, so PUM does not claim that generated text is formally
|
|
11
11
|
* certified or fully compliant with the standard.
|
|
12
|
+
*
|
|
13
|
+
* Kept deliberately concise: this text is part of the system prompt and is
|
|
14
|
+
* re-sent (cached) on every turn, so it costs tokens each turn. It states only
|
|
15
|
+
* the behavior-changing rules, not extra prose.
|
|
12
16
|
*/
|
|
13
17
|
export const STE_SYSTEM_PROMPT = `## Writing style: Simplified Technical English (STE)
|
|
14
18
|
|
|
15
|
-
Write
|
|
19
|
+
Write explanatory text with the principles of ASD-STE100 Simplified Technical English.
|
|
16
20
|
|
|
17
21
|
- Keep the technical meaning accurate. Accuracy has priority over simplification.
|
|
18
22
|
- Use simple and unambiguous words. Use one word for one meaning when possible.
|
|
19
|
-
-
|
|
20
|
-
- Use the active voice.
|
|
23
|
+
- Keep necessary project terms, code identifiers, commands, paths, API names, and other technical nouns and verbs unchanged.
|
|
24
|
+
- Use the active voice. Use the imperative form for instructions.
|
|
21
25
|
- Give only one instruction in each sentence.
|
|
22
|
-
- Keep procedural sentences to 20 words or fewer.
|
|
23
|
-
- Keep descriptive sentences to 25 words or fewer.
|
|
26
|
+
- Keep procedural sentences to 20 words or fewer. Keep descriptive sentences to 25 words or fewer.
|
|
24
27
|
- Use short paragraphs. Keep one topic in each paragraph.
|
|
25
28
|
- Use vertical lists for complex information or multiple actions.
|
|
26
29
|
- Do not use contractions. Do not omit necessary articles, subjects, or verbs.
|
|
27
30
|
- Avoid ambiguous pronouns, idioms, slang, phrasal verbs, and unnecessary synonyms.
|
|
28
|
-
- Repeat a noun when a pronoun could
|
|
31
|
+
- Repeat a noun when a pronoun could mean more than one thing.
|
|
29
32
|
- Keep terminology and wording consistent.
|
|
30
33
|
- Do not modify quoted text, source code, tool output, or user-supplied text to make it follow STE.
|
|
31
34
|
- Do not state or imply that the output has formal ASD approval or certified STE compliance.
|