@tryinget/pi-activity-strip 0.2.0
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/LICENSE +78 -0
- package/README.md +203 -0
- package/bin/pi-activity-strip.mjs +226 -0
- package/examples/simulate-sessions.mjs +64 -0
- package/extensions/activity-strip.js +202 -0
- package/extensions/activity-strip.ts +1 -0
- package/package.json +94 -0
- package/policy/security-policy.json +10 -0
- package/prompts/pi-activity-strip-implementation-planning.md +21 -0
- package/prompts/pi-activity-strip-security-review.md +21 -0
- package/src/broker/server.mjs +165 -0
- package/src/broker/session-store.mjs +49 -0
- package/src/client/broker-client.mjs +115 -0
- package/src/client/launcher.mjs +116 -0
- package/src/client/session-telemetry.mjs +214 -0
- package/src/common/compatibility.mjs +153 -0
- package/src/common/constants.mjs +35 -0
- package/src/common/contracts.d.ts +142 -0
- package/src/common/contracts.ts +142 -0
- package/src/common/electron.mjs +51 -0
- package/src/common/format.mjs +75 -0
- package/src/common/protocol.mjs +71 -0
- package/src/common/status-report.mjs +58 -0
- package/src/common/telemetry.mjs +180 -0
- package/src/electron/main.mjs +319 -0
- package/src/electron/preload.cjs +10 -0
- package/src/ui/strip-html.mjs +375 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type SessionState = "idle" | "thinking" | "tool" | "waiting" | "success" | "error";
|
|
2
|
+
|
|
3
|
+
export interface SessionSnapshot {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
processId: number;
|
|
6
|
+
cwd: string;
|
|
7
|
+
repoLabel: string;
|
|
8
|
+
sessionName: string;
|
|
9
|
+
phase: string;
|
|
10
|
+
detail: string;
|
|
11
|
+
assistantPreview: string;
|
|
12
|
+
toolName: string;
|
|
13
|
+
toolTarget: string;
|
|
14
|
+
state: SessionState;
|
|
15
|
+
turnIndex: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
startedAt: number;
|
|
18
|
+
agentStartedAt: number | null;
|
|
19
|
+
agentActive: boolean;
|
|
20
|
+
lastPromptPreview: string;
|
|
21
|
+
errorMessage: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BrokerSnapshot {
|
|
25
|
+
generatedAt: number;
|
|
26
|
+
sessions: SessionSnapshot[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ActivityStripRuntimeState = "starting" | "ready" | "error";
|
|
30
|
+
|
|
31
|
+
export interface ActivityStripRuntimeStatus {
|
|
32
|
+
state: ActivityStripRuntimeState;
|
|
33
|
+
startedAt: number;
|
|
34
|
+
readyAt?: number | null;
|
|
35
|
+
windowVisible?: boolean;
|
|
36
|
+
displayServer?: "wayland" | "x11" | "headless" | "unknown";
|
|
37
|
+
windowManager?: string | null;
|
|
38
|
+
displayCount?: number | null;
|
|
39
|
+
alignmentMode?: "niri" | "generic";
|
|
40
|
+
warnings?: string[];
|
|
41
|
+
error?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BrokerResponse {
|
|
45
|
+
ok?: boolean;
|
|
46
|
+
type?: string;
|
|
47
|
+
snapshot?: BrokerSnapshot;
|
|
48
|
+
runtimeStatus?: ActivityStripRuntimeStatus;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BrokerClientOptions {
|
|
53
|
+
expectReply?: boolean;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
socketPath?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SessionStoreOptions {
|
|
59
|
+
staleAfterMs?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ActivityStripBrokerOptions {
|
|
63
|
+
socketPath?: string;
|
|
64
|
+
socketDir?: string;
|
|
65
|
+
store?: {
|
|
66
|
+
snapshot(): BrokerSnapshot;
|
|
67
|
+
upsert(session: Partial<SessionSnapshot> | Record<string, unknown>): boolean;
|
|
68
|
+
remove(sessionId: string): boolean;
|
|
69
|
+
};
|
|
70
|
+
getRuntimeStatus?: () => ActivityStripRuntimeStatus | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ToolCallDescription {
|
|
74
|
+
state: "tool" | "waiting";
|
|
75
|
+
phase: string;
|
|
76
|
+
detail: string;
|
|
77
|
+
toolTarget?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ToolResultSummary {
|
|
81
|
+
state: "thinking" | "error";
|
|
82
|
+
phase: string;
|
|
83
|
+
detail: string;
|
|
84
|
+
errorMessage?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TelemetryPiLike {
|
|
88
|
+
getSessionName?: () => string | undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SessionStartContextLike {
|
|
92
|
+
cwd?: string;
|
|
93
|
+
hasUI?: boolean;
|
|
94
|
+
ui?: {
|
|
95
|
+
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
96
|
+
editor?: (title: string, text: string) => Promise<unknown>;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BeforeAgentStartEventLike {
|
|
101
|
+
prompt?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TurnStartEventLike {
|
|
105
|
+
turnIndex?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface AssistantMessageEventLike {
|
|
109
|
+
type?: string;
|
|
110
|
+
delta?: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface MessageUpdateEventLike {
|
|
114
|
+
assistantMessageEvent?: AssistantMessageEventLike;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ToolExecutionEventLike {
|
|
118
|
+
toolName?: string;
|
|
119
|
+
args?: Record<string, unknown>;
|
|
120
|
+
partialResult?: unknown;
|
|
121
|
+
result?: unknown;
|
|
122
|
+
isError?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SessionTelemetryOptions {
|
|
126
|
+
pi?: TelemetryPiLike;
|
|
127
|
+
cwd?: string;
|
|
128
|
+
sessionName?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ActivityStripCompatibilityReport {
|
|
132
|
+
ok: boolean;
|
|
133
|
+
displayServer: "wayland" | "x11" | "headless" | "unknown";
|
|
134
|
+
windowManager: string | null;
|
|
135
|
+
electronPath: string | null;
|
|
136
|
+
displayCount: number | null;
|
|
137
|
+
alignmentMode: "niri" | "generic";
|
|
138
|
+
primaryDisplayOnly: boolean;
|
|
139
|
+
clickThroughDefault: boolean;
|
|
140
|
+
blockers: string[];
|
|
141
|
+
warnings: string[];
|
|
142
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_ELECTRON_CANDIDATES } from "./constants.mjs";
|
|
5
|
+
|
|
6
|
+
/** @param {string} targetPath */
|
|
7
|
+
async function canExecute(targetPath) {
|
|
8
|
+
try {
|
|
9
|
+
await access(targetPath, constants.X_OK);
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {string} binaryName */
|
|
17
|
+
async function findOnPath(binaryName) {
|
|
18
|
+
for (const entry of String(process.env.PATH ?? "")
|
|
19
|
+
.split(":")
|
|
20
|
+
.filter(Boolean)) {
|
|
21
|
+
const candidate = path.join(entry, binaryName);
|
|
22
|
+
if (await canExecute(candidate)) return candidate;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {string | null} [explicitPath] */
|
|
28
|
+
export async function locateElectron(explicitPath = null) {
|
|
29
|
+
if (explicitPath) {
|
|
30
|
+
if (await canExecute(explicitPath)) return explicitPath;
|
|
31
|
+
throw new Error(`Configured Electron binary is not executable: ${explicitPath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const envBinary = process.env.GLIMPSE_ELECTRON_BIN || process.env.PI_ACTIVITY_STRIP_ELECTRON_BIN;
|
|
35
|
+
if (envBinary) {
|
|
36
|
+
if (await canExecute(envBinary)) return envBinary;
|
|
37
|
+
throw new Error(`Configured Electron binary is not executable: ${envBinary}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const localBinary = path.resolve(process.cwd(), "node_modules", ".bin", "electron");
|
|
41
|
+
if (await canExecute(localBinary)) return localBinary;
|
|
42
|
+
|
|
43
|
+
for (const candidate of DEFAULT_ELECTRON_CANDIDATES) {
|
|
44
|
+
const found = await findOnPath(candidate);
|
|
45
|
+
if (found) return found;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Could not find an Electron binary. Install electron (or electron39 on Arch) or set GLIMPSE_ELECTRON_BIN / PI_ACTIVITY_STRIP_ELECTRON_BIN.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping requires an ESC control character pattern.
|
|
4
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
5
|
+
|
|
6
|
+
/** @param {unknown} value @param {number} [max] */
|
|
7
|
+
export function truncate(value, max = 96) {
|
|
8
|
+
const text = String(value ?? "").trim();
|
|
9
|
+
if (text.length <= max) return text;
|
|
10
|
+
return `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @param {unknown} value */
|
|
14
|
+
export function stripAnsi(value) {
|
|
15
|
+
return String(value ?? "").replaceAll(ANSI_RE, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {unknown} value */
|
|
19
|
+
export function compactWhitespace(value) {
|
|
20
|
+
return stripAnsi(String(value ?? ""))
|
|
21
|
+
.replace(/\s+/g, " ")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {unknown} value */
|
|
26
|
+
export function lastNonEmptyLine(value) {
|
|
27
|
+
const lines = String(value ?? "")
|
|
28
|
+
.split(/\r?\n/g)
|
|
29
|
+
.map((line) => compactWhitespace(line))
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
return lines.at(-1) ?? "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {unknown} targetPath */
|
|
35
|
+
export function basenameLabel(targetPath) {
|
|
36
|
+
const normalized = String(targetPath ?? "").trim();
|
|
37
|
+
if (!normalized) return "(unknown)";
|
|
38
|
+
return path.basename(normalized) || normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {unknown} targetPath @param {number} [max] */
|
|
42
|
+
export function previewPath(targetPath, max = 64) {
|
|
43
|
+
const normalized = compactWhitespace(targetPath);
|
|
44
|
+
if (!normalized) return "";
|
|
45
|
+
if (normalized.length <= max) return normalized;
|
|
46
|
+
|
|
47
|
+
const base = path.basename(normalized);
|
|
48
|
+
if (base.length + 4 >= max) {
|
|
49
|
+
return truncate(base, max);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return truncate(`…/${base}`, max);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {unknown} command @param {number} [max] */
|
|
56
|
+
export function previewCommand(command, max = 72) {
|
|
57
|
+
return truncate(compactWhitespace(command), max);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param {unknown} value @param {number} [max] */
|
|
61
|
+
export function previewText(value, max = 96) {
|
|
62
|
+
return truncate(lastNonEmptyLine(value) || compactWhitespace(value), max);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {unknown} cwd @param {unknown} explicitSessionName */
|
|
66
|
+
export function formatRepoLabel(cwd, explicitSessionName) {
|
|
67
|
+
const sessionName = compactWhitespace(explicitSessionName);
|
|
68
|
+
if (sessionName) return truncate(sessionName, 36);
|
|
69
|
+
|
|
70
|
+
const normalized = String(cwd ?? "").trim();
|
|
71
|
+
if (!normalized) return "pi session";
|
|
72
|
+
|
|
73
|
+
const repo = path.basename(normalized) || normalized;
|
|
74
|
+
return truncate(repo, 36);
|
|
75
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./contracts.ts").SessionState} SessionState */
|
|
4
|
+
/** @typedef {import("./contracts.ts").SessionSnapshot} SessionSnapshot */
|
|
5
|
+
|
|
6
|
+
/** @type {Set<SessionState>} */
|
|
7
|
+
const SESSION_STATES = new Set(["idle", "thinking", "tool", "waiting", "success", "error"]);
|
|
8
|
+
|
|
9
|
+
/** @param {string} type @param {Record<string, unknown>} [payload] */
|
|
10
|
+
export function makeMessage(type, payload = {}) {
|
|
11
|
+
return {
|
|
12
|
+
id: randomUUID(),
|
|
13
|
+
type,
|
|
14
|
+
...payload,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {Partial<SessionSnapshot> | Record<string, unknown>} [session] @returns {SessionSnapshot} */
|
|
19
|
+
export function normalizeSessionSnapshot(session = {}) {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const updatedAt = Number(session.updatedAt ?? now) || now;
|
|
22
|
+
const startedAt = Number(session.startedAt ?? updatedAt) || updatedAt;
|
|
23
|
+
const agentStartedAt =
|
|
24
|
+
session.agentStartedAt == null ? null : Number(session.agentStartedAt) || null;
|
|
25
|
+
const rawState = typeof session.state === "string" ? session.state : "idle";
|
|
26
|
+
/** @type {SessionState} */
|
|
27
|
+
const state = SESSION_STATES.has(/** @type {SessionState} */ (rawState))
|
|
28
|
+
? /** @type {SessionState} */ (rawState)
|
|
29
|
+
: "idle";
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
sessionId: String(session.sessionId ?? ""),
|
|
33
|
+
processId: Number(session.processId ?? 0) || 0,
|
|
34
|
+
cwd: String(session.cwd ?? ""),
|
|
35
|
+
repoLabel: String(session.repoLabel ?? "pi session"),
|
|
36
|
+
sessionName: String(session.sessionName ?? ""),
|
|
37
|
+
phase: String(session.phase ?? "Idle"),
|
|
38
|
+
detail: String(session.detail ?? "Ready"),
|
|
39
|
+
assistantPreview: String(session.assistantPreview ?? ""),
|
|
40
|
+
toolName: String(session.toolName ?? ""),
|
|
41
|
+
toolTarget: String(session.toolTarget ?? ""),
|
|
42
|
+
state,
|
|
43
|
+
turnIndex: Number(session.turnIndex ?? 0) || 0,
|
|
44
|
+
updatedAt,
|
|
45
|
+
startedAt,
|
|
46
|
+
agentStartedAt,
|
|
47
|
+
agentActive: Boolean(session.agentActive),
|
|
48
|
+
lastPromptPreview: String(session.lastPromptPreview ?? ""),
|
|
49
|
+
errorMessage: String(session.errorMessage ?? ""),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {SessionSnapshot[]} [sessions] @returns {SessionSnapshot[]} */
|
|
54
|
+
export function sortSessions(sessions = []) {
|
|
55
|
+
const stateWeight = new Map([
|
|
56
|
+
["tool", 0],
|
|
57
|
+
["thinking", 1],
|
|
58
|
+
["waiting", 2],
|
|
59
|
+
["error", 3],
|
|
60
|
+
["success", 4],
|
|
61
|
+
["idle", 5],
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
return [...sessions].sort((left, right) => {
|
|
65
|
+
const leftWeight = stateWeight.get(left.state) ?? 99;
|
|
66
|
+
const rightWeight = stateWeight.get(right.state) ?? 99;
|
|
67
|
+
if (leftWeight !== rightWeight) return leftWeight - rightWeight;
|
|
68
|
+
if (left.updatedAt !== right.updatedAt) return right.updatedAt - left.updatedAt;
|
|
69
|
+
return left.repoLabel.localeCompare(right.repoLabel);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @typedef {import("./contracts.ts").BrokerResponse} BrokerResponse */
|
|
2
|
+
|
|
3
|
+
/** @param {BrokerResponse | null | undefined} result */
|
|
4
|
+
export function formatBrokerRuntimeStatus(result) {
|
|
5
|
+
if (!result?.ok) return "stopped";
|
|
6
|
+
const runtime = result.runtimeStatus;
|
|
7
|
+
if (!runtime) return "running (legacy broker: no overlay readiness metadata)";
|
|
8
|
+
|
|
9
|
+
const lines = [
|
|
10
|
+
runtime.state === "ready" ? "running (ready)" : `running (${runtime.state})`,
|
|
11
|
+
`display: ${runtime.displayServer || "unknown"}`,
|
|
12
|
+
`window manager: ${runtime.windowManager || "unknown"}`,
|
|
13
|
+
`alignment: ${runtime.alignmentMode || "unknown"}`,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
if (typeof runtime.displayCount === "number") {
|
|
17
|
+
lines.push(`displays: ${runtime.displayCount}`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof runtime.windowVisible === "boolean") {
|
|
20
|
+
lines.push(`window visible: ${runtime.windowVisible ? "yes" : "no"}`);
|
|
21
|
+
}
|
|
22
|
+
if (runtime.error) {
|
|
23
|
+
lines.push(`error: ${runtime.error}`);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(runtime.warnings) && runtime.warnings.length > 0) {
|
|
26
|
+
lines.push("warnings:");
|
|
27
|
+
for (const warning of runtime.warnings) {
|
|
28
|
+
lines.push(`- ${warning}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {BrokerResponse | null | undefined} result
|
|
37
|
+
* @returns {{ headline: string; level: "info" | "warning" | "error" }}
|
|
38
|
+
*/
|
|
39
|
+
export function summarizeBrokerRuntimeStatus(result) {
|
|
40
|
+
if (!result?.ok) {
|
|
41
|
+
return { headline: "Activity strip is stopped", level: "warning" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const runtime = result.runtimeStatus;
|
|
45
|
+
if (!runtime) {
|
|
46
|
+
return { headline: "Activity strip is running", level: "info" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (runtime.state === "ready") {
|
|
50
|
+
return { headline: "Activity strip is running and ready", level: "info" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (runtime.state === "error") {
|
|
54
|
+
return { headline: "Activity strip reported an error", level: "error" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { headline: `Activity strip is ${runtime.state}`, level: "warning" };
|
|
58
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
/** @typedef {import("./contracts.ts").SessionSnapshot} SessionSnapshot */
|
|
4
|
+
/** @typedef {import("./contracts.ts").ToolCallDescription} ToolCallDescription */
|
|
5
|
+
/** @typedef {import("./contracts.ts").ToolResultSummary} ToolResultSummary */
|
|
6
|
+
import {
|
|
7
|
+
basenameLabel,
|
|
8
|
+
compactWhitespace,
|
|
9
|
+
formatRepoLabel,
|
|
10
|
+
previewCommand,
|
|
11
|
+
previewPath,
|
|
12
|
+
previewText,
|
|
13
|
+
truncate,
|
|
14
|
+
} from "./format.mjs";
|
|
15
|
+
|
|
16
|
+
/** @param {unknown} value */
|
|
17
|
+
function asRecord(value) {
|
|
18
|
+
return value && typeof value === "object" ? /** @type {Record<string, unknown>} */ (value) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @param {unknown} value @param {number} [depth] @returns {string} */
|
|
22
|
+
export function extractToolResultText(value, depth = 0) {
|
|
23
|
+
if (value == null || depth > 4) return "";
|
|
24
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value
|
|
29
|
+
.map((entry) => extractToolResultText(entry, depth + 1))
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const record = asRecord(value);
|
|
35
|
+
if (!record) return "";
|
|
36
|
+
if (record.type === "text" && typeof record.text === "string") return record.text;
|
|
37
|
+
|
|
38
|
+
for (const key of [
|
|
39
|
+
"errorMessage",
|
|
40
|
+
"error",
|
|
41
|
+
"message",
|
|
42
|
+
"stdout",
|
|
43
|
+
"stderr",
|
|
44
|
+
"output",
|
|
45
|
+
"content",
|
|
46
|
+
"details",
|
|
47
|
+
"text",
|
|
48
|
+
]) {
|
|
49
|
+
const text = extractToolResultText(record[key], depth + 1);
|
|
50
|
+
if (text) return text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
} catch {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createSessionId() {
|
|
61
|
+
return `${os.hostname()}-${process.pid}-${Date.now()}-${randomUUID().slice(0, 6)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {{ cwd?: string; sessionName?: string }} [options] @returns {SessionSnapshot} */
|
|
65
|
+
export function createInitialSnapshot({ cwd = process.cwd(), sessionName = "" } = {}) {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
return {
|
|
68
|
+
sessionId: createSessionId(),
|
|
69
|
+
processId: process.pid,
|
|
70
|
+
cwd,
|
|
71
|
+
repoLabel: formatRepoLabel(cwd, sessionName),
|
|
72
|
+
sessionName: compactWhitespace(sessionName),
|
|
73
|
+
phase: "Idle",
|
|
74
|
+
detail: previewPath(cwd, 72) || "Ready",
|
|
75
|
+
assistantPreview: "",
|
|
76
|
+
toolName: "",
|
|
77
|
+
toolTarget: "",
|
|
78
|
+
state: "idle",
|
|
79
|
+
turnIndex: 0,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
startedAt: now,
|
|
82
|
+
agentStartedAt: null,
|
|
83
|
+
agentActive: false,
|
|
84
|
+
lastPromptPreview: "",
|
|
85
|
+
errorMessage: "",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {string} toolName @param {Record<string, unknown>} [args] @returns {ToolCallDescription} */
|
|
90
|
+
export function describeToolCall(toolName, args = {}) {
|
|
91
|
+
switch (toolName) {
|
|
92
|
+
case "bash":
|
|
93
|
+
return {
|
|
94
|
+
state: "tool",
|
|
95
|
+
phase: "Running bash",
|
|
96
|
+
detail: previewCommand(args.command, 104) || "Running shell command",
|
|
97
|
+
toolTarget: previewCommand(args.command, 72),
|
|
98
|
+
};
|
|
99
|
+
case "read":
|
|
100
|
+
return {
|
|
101
|
+
state: "tool",
|
|
102
|
+
phase: "Reading file",
|
|
103
|
+
detail: previewPath(args.path, 104) || "Reading file",
|
|
104
|
+
toolTarget: previewPath(args.path, 72),
|
|
105
|
+
};
|
|
106
|
+
case "write":
|
|
107
|
+
return {
|
|
108
|
+
state: "tool",
|
|
109
|
+
phase: "Writing file",
|
|
110
|
+
detail: previewPath(args.path, 104) || "Writing file",
|
|
111
|
+
toolTarget: previewPath(args.path, 72),
|
|
112
|
+
};
|
|
113
|
+
case "edit":
|
|
114
|
+
return {
|
|
115
|
+
state: "tool",
|
|
116
|
+
phase: "Editing file",
|
|
117
|
+
detail: previewPath(args.path, 104) || "Editing file",
|
|
118
|
+
toolTarget: previewPath(args.path, 72),
|
|
119
|
+
};
|
|
120
|
+
case "interview":
|
|
121
|
+
return {
|
|
122
|
+
state: "waiting",
|
|
123
|
+
phase: "Waiting for input",
|
|
124
|
+
detail: truncate("Interactive form is open", 104),
|
|
125
|
+
toolTarget: "form",
|
|
126
|
+
};
|
|
127
|
+
case "copy_to_clipboard":
|
|
128
|
+
return {
|
|
129
|
+
state: "tool",
|
|
130
|
+
phase: "Copying to clipboard",
|
|
131
|
+
detail: truncate("Preparing clipboard payload", 104),
|
|
132
|
+
toolTarget: "clipboard",
|
|
133
|
+
};
|
|
134
|
+
default:
|
|
135
|
+
return {
|
|
136
|
+
state: "tool",
|
|
137
|
+
phase: `Running ${toolName}`,
|
|
138
|
+
detail: previewText(JSON.stringify(args), 104) || `Running ${toolName}`,
|
|
139
|
+
toolTarget: basenameLabel(toolName),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @param {string} toolName @param {unknown} result @param {boolean} [isError] @returns {ToolResultSummary} */
|
|
145
|
+
export function summarizeToolResult(toolName, result, isError = false) {
|
|
146
|
+
const record = asRecord(result);
|
|
147
|
+
if (isError) {
|
|
148
|
+
const errorText = extractToolResultText(
|
|
149
|
+
record?.errorMessage ??
|
|
150
|
+
record?.error ??
|
|
151
|
+
record?.message ??
|
|
152
|
+
record?.details ??
|
|
153
|
+
record?.content ??
|
|
154
|
+
result,
|
|
155
|
+
);
|
|
156
|
+
return {
|
|
157
|
+
state: "error",
|
|
158
|
+
phase: `${toolName} failed`,
|
|
159
|
+
detail: previewText(errorText, 104) || `${toolName} failed`,
|
|
160
|
+
errorMessage: previewText(errorText, 104),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const outputText = extractToolResultText(result);
|
|
165
|
+
if (toolName === "bash") {
|
|
166
|
+
return {
|
|
167
|
+
state: "thinking",
|
|
168
|
+
phase: "Processing output",
|
|
169
|
+
detail: previewText(outputText, 104) || "Command finished",
|
|
170
|
+
errorMessage: "",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
state: "thinking",
|
|
176
|
+
phase: "Continuing",
|
|
177
|
+
detail: previewText(outputText, 104) || `${toolName} finished`,
|
|
178
|
+
errorMessage: "",
|
|
179
|
+
};
|
|
180
|
+
}
|