march-cli 0.1.38 → 0.1.40
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/agent/runner.mjs +8 -8
- package/src/agent/runtime/runner-process-factory.mjs +1 -1
- package/src/agent/turn/turn-runner.mjs +4 -4
- package/src/cli/args.mjs +3 -0
- package/src/cli/commands/mode-command.mjs +1 -0
- package/src/cli/commands/registry/slash-command-registry.mjs +2 -3
- package/src/cli/input/keybindings.mjs +2 -0
- package/src/cli/repl-commands.mjs +1 -1
- package/src/cli/repl-loop.mjs +1 -1
- package/src/cli/session/pi-session-switch-command.mjs +11 -11
- package/src/cli/session/session-list-command.mjs +1 -1
- package/src/cli/session/session-source-command.mjs +0 -76
- package/src/cli/startup/app-runtime.mjs +52 -22
- package/src/cli/tui/output/timeline-block-restore.mjs +45 -0
- package/src/cli/tui/output-buffer.mjs +5 -0
- package/src/cli/tui/tui-input-controller.mjs +16 -0
- package/src/cli/ui.mjs +6 -2
- package/src/cli/workspace/command.mjs +11 -37
- package/src/cli/workspace/output-router.mjs +62 -36
- package/src/cli/workspace/project-runtime.mjs +2 -0
- package/src/cli/workspace/runtime-session-state.mjs +9 -0
- package/src/cli/workspace/tui-timeline-projection.mjs +179 -0
- package/src/cli/workspace/tui-timeline.mjs +247 -0
- package/src/extensions/lifecycle-adapter.mjs +2 -2
- package/src/main.mjs +7 -1
- package/src/session/sidecar-sync.mjs +3 -17
- package/src/session/sidecar.mjs +40 -41
- package/src/session/state/march-session-state.mjs +165 -0
- package/src/session/state/march-session-sync.mjs +20 -0
- package/src/session/state/march-session-ui-state.mjs +89 -0
- package/src/workspace/session-index.mjs +27 -0
- package/src/workspace/supervisor.mjs +19 -13
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +0 -10
- package/src/cli/session/session-switch-command.mjs +0 -1
- package/src/session/persist.mjs +0 -1
|
@@ -3,8 +3,6 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { brightBlack } from "../tui/ui-theme.mjs";
|
|
4
4
|
import { registerProject, listRegisteredProjects } from "../../workspace/project-registry.mjs";
|
|
5
5
|
import { buildWorkspaceSessionSelectItems, listWorkspaceSessions, workspaceSessionSearchText } from "../../workspace/session-index.mjs";
|
|
6
|
-
import { resumePiSessionById } from "../session/pi-session-switch-command.mjs";
|
|
7
|
-
import { loadPiSessionTranscriptTurns } from "../../session/transcript.mjs";
|
|
8
6
|
|
|
9
7
|
export const WORKSPACE_SLASH_COMMANDS = [
|
|
10
8
|
{
|
|
@@ -19,9 +17,9 @@ export const WORKSPACE_SLASH_COMMANDS = [
|
|
|
19
17
|
run: async (ctx, command) => writeLines(ctx.ui, await handleProjectCommand(command, ctx)),
|
|
20
18
|
},
|
|
21
19
|
{
|
|
22
|
-
metadata: [{ name: "
|
|
23
|
-
match: (trimmed) => trimmed === "/
|
|
24
|
-
run:
|
|
20
|
+
metadata: [{ name: "session", description: "Open workspace session selector" }],
|
|
21
|
+
match: (trimmed) => trimmed === "/session" ? { parsed: { type: "session" } } : null,
|
|
22
|
+
run: handleSessionCommand,
|
|
25
23
|
},
|
|
26
24
|
];
|
|
27
25
|
|
|
@@ -45,9 +43,9 @@ export async function handleProjectCommand(command, { stateRoot }) {
|
|
|
45
43
|
return ["Registered projects:", ...projects.map((project) => `- ${project.displayName} ${brightBlack(project.rootPath)}`)];
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
export async function
|
|
46
|
+
export async function handleSessionCommand({ stateRoot, currentProjectId, runner, workspaceSupervisor, workspaceOutputRouter, ui }) {
|
|
49
47
|
if (!stateRoot) {
|
|
50
|
-
ui.writeln("Session
|
|
48
|
+
ui.writeln("Session selector is not available: workspace registry is missing.");
|
|
51
49
|
return { handled: true };
|
|
52
50
|
}
|
|
53
51
|
const projects = await listWorkspaceSessions({ stateRoot, currentProjectId });
|
|
@@ -59,7 +57,7 @@ export async function handleSwitchCommand({ stateRoot, currentProjectId, project
|
|
|
59
57
|
return { handled: true };
|
|
60
58
|
}
|
|
61
59
|
if (!ui.selectList) {
|
|
62
|
-
ui.writeln("Session
|
|
60
|
+
ui.writeln("Session selector is only available in TUI.");
|
|
63
61
|
return { handled: true };
|
|
64
62
|
}
|
|
65
63
|
const selectedIndex = Math.max(0, items.findIndex((item) => item.project.current && item.session?.id === currentSessionId));
|
|
@@ -82,7 +80,6 @@ export async function handleSwitchCommand({ stateRoot, currentProjectId, project
|
|
|
82
80
|
}
|
|
83
81
|
try {
|
|
84
82
|
const { result } = await workspaceSupervisor.startNewWorkspaceSession(item.project);
|
|
85
|
-
ui.restoreTranscript?.([]);
|
|
86
83
|
ui.writeln(`Created session: ${item.project.displayName} / ${result?.sessionId ?? "new session"}`);
|
|
87
84
|
return { handled: true, refreshContextTokens: true, activeChanged: true };
|
|
88
85
|
} catch (err) {
|
|
@@ -93,25 +90,15 @@ export async function handleSwitchCommand({ stateRoot, currentProjectId, project
|
|
|
93
90
|
if (workspaceSupervisor) {
|
|
94
91
|
try {
|
|
95
92
|
await workspaceSupervisor.activateWorkspaceSession({ project: item.project, session: item.session });
|
|
96
|
-
|
|
97
|
-
const replayed = ctxReplayBufferedOutput({ workspaceOutputRouter, projectId: item.project.projectId, sessionId: item.session.id });
|
|
98
|
-
ui.writeln(`Switched to session: ${item.project.displayName} / ${item.session.name || item.session.id}${replayed ? ` (${replayed} buffered events replayed)` : ""}`);
|
|
93
|
+
ctxRenderActiveSession({ workspaceOutputRouter, projectId: item.project.projectId, sessionId: item.session.id });
|
|
99
94
|
return { handled: true, refreshContextTokens: true, activeChanged: true };
|
|
100
95
|
} catch (err) {
|
|
101
96
|
ui.writeln(`Error: ${err.message}`);
|
|
102
97
|
return { handled: true };
|
|
103
98
|
}
|
|
104
99
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
ui.writeln(brightBlack("Cross-project attach requires the workspace supervisor."));
|
|
108
|
-
return { handled: true };
|
|
109
|
-
}
|
|
110
|
-
const sessions = projects.find((project) => project.current)?.sessions ?? [];
|
|
111
|
-
const lines = await resumePiSessionById(item.session.id, { runner, sessions, projectMarchDir });
|
|
112
|
-
if (isResumeSuccess(lines)) restoreTranscriptFromSession(item.session, ui);
|
|
113
|
-
for (const line of lines) ui.writeln(line);
|
|
114
|
-
return { handled: true, refreshContextTokens: isResumeSuccess(lines) };
|
|
100
|
+
ui.writeln("Workspace session activation requires the workspace supervisor.");
|
|
101
|
+
return { handled: true };
|
|
115
102
|
}
|
|
116
103
|
|
|
117
104
|
function annotateWorkspaceItems(items, runtimeSummaries) {
|
|
@@ -124,24 +111,11 @@ function annotateWorkspaceItems(items, runtimeSummaries) {
|
|
|
124
111
|
});
|
|
125
112
|
}
|
|
126
113
|
|
|
127
|
-
function
|
|
128
|
-
return workspaceOutputRouter?.
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function restoreTranscriptFromSession(session, ui) {
|
|
132
|
-
if (typeof ui.restoreTranscript !== "function") return;
|
|
133
|
-
try {
|
|
134
|
-
ui.restoreTranscript(loadPiSessionTranscriptTurns(session.path));
|
|
135
|
-
} catch (err) {
|
|
136
|
-
ui.writeln(`Warning: failed to restore session transcript: ${err.message}`);
|
|
137
|
-
}
|
|
114
|
+
function ctxRenderActiveSession({ workspaceOutputRouter, projectId, sessionId }) {
|
|
115
|
+
return workspaceOutputRouter?.getRenderEventCount?.(projectId, sessionId) ?? 0;
|
|
138
116
|
}
|
|
139
117
|
|
|
140
118
|
function writeLines(ui, lines) {
|
|
141
119
|
for (const line of lines) ui.writeln(line);
|
|
142
120
|
return { handled: true };
|
|
143
121
|
}
|
|
144
|
-
|
|
145
|
-
function isResumeSuccess(lines) {
|
|
146
|
-
return Array.isArray(lines) && lines.some((line) => String(line).startsWith("Resumed pi session:"));
|
|
147
|
-
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { createTuiTimelineRegistry } from "./tui-timeline.mjs";
|
|
2
|
+
|
|
3
|
+
const PERSIST_FLUSH_METHODS = new Set(["turnEnd", "assistantReplyEnd", "toolEnd"]);
|
|
4
|
+
|
|
5
|
+
const RENDER_METHODS = new Set([
|
|
2
6
|
"turnStart",
|
|
3
7
|
"turnEnd",
|
|
4
8
|
"assistantReplyEnd",
|
|
@@ -6,28 +10,43 @@ const BACKGROUND_METHODS_TO_BUFFER = new Set([
|
|
|
6
10
|
"thinkingStart",
|
|
7
11
|
"thinkingDelta",
|
|
8
12
|
"thinkingEnd",
|
|
13
|
+
"thinkingBlock",
|
|
9
14
|
"toolStart",
|
|
10
15
|
"toolEnd",
|
|
11
16
|
"retryStart",
|
|
12
17
|
"retryEnd",
|
|
13
18
|
"status",
|
|
14
|
-
"debugLines",
|
|
15
19
|
"recall",
|
|
16
|
-
"providerQuotaSnapshot",
|
|
17
20
|
"editDiff",
|
|
21
|
+
"write",
|
|
18
22
|
"writeln",
|
|
23
|
+
"clearOutput",
|
|
19
24
|
]);
|
|
20
25
|
|
|
21
|
-
export function createWorkspaceOutputRouter({
|
|
26
|
+
export function createWorkspaceOutputRouter({
|
|
27
|
+
ui,
|
|
28
|
+
activeProjectId,
|
|
29
|
+
activeSessionId = null,
|
|
30
|
+
onPersistRenderTimeline = null,
|
|
31
|
+
persistDebounceMs,
|
|
32
|
+
} = {}) {
|
|
22
33
|
let active = routeKey(activeProjectId, activeSessionId);
|
|
23
|
-
const
|
|
34
|
+
const timelineRegistry = createTuiTimelineRegistry({
|
|
35
|
+
persistDebounceMs,
|
|
36
|
+
onPersistTimeline: (change) => onPersistRenderTimeline?.({ ...parseRouteKey(change.key), ...change }),
|
|
37
|
+
});
|
|
24
38
|
|
|
25
39
|
return {
|
|
26
40
|
setActiveProject(projectId) {
|
|
27
|
-
|
|
41
|
+
this.setActiveSession(projectId, null);
|
|
28
42
|
},
|
|
29
|
-
setActiveSession(projectId, sessionId) {
|
|
30
|
-
|
|
43
|
+
setActiveSession(projectId, sessionId, { renderTimeline = null } = {}) {
|
|
44
|
+
const next = routeKey(projectId, sessionId);
|
|
45
|
+
timelineRegistry.ensure(next, { events: renderTimeline });
|
|
46
|
+
if (next === active) return renderRoute(next);
|
|
47
|
+
timelineRegistry.flush(active, "session-switch");
|
|
48
|
+
active = next;
|
|
49
|
+
return renderRoute(next);
|
|
31
50
|
},
|
|
32
51
|
getActiveRouteKey() {
|
|
33
52
|
return active;
|
|
@@ -46,8 +65,9 @@ export function createWorkspaceOutputRouter({ ui, activeProjectId, activeSession
|
|
|
46
65
|
if (typeof value !== "function") return value;
|
|
47
66
|
return (...args) => {
|
|
48
67
|
const key = routeKey(projectId, typeof getSessionId === "function" ? getSessionId() : sessionId);
|
|
49
|
-
if (
|
|
50
|
-
|
|
68
|
+
if (!RENDER_METHODS.has(prop)) return value.apply(ui, args);
|
|
69
|
+
recordRenderEvent(key, prop, args);
|
|
70
|
+
if (key === active) return value.apply(ui, args);
|
|
51
71
|
return undefined;
|
|
52
72
|
};
|
|
53
73
|
},
|
|
@@ -60,41 +80,47 @@ export function createWorkspaceOutputRouter({ ui, activeProjectId, activeSession
|
|
|
60
80
|
},
|
|
61
81
|
});
|
|
62
82
|
},
|
|
63
|
-
|
|
64
|
-
return
|
|
83
|
+
renderActiveSession() {
|
|
84
|
+
return renderRoute(active);
|
|
65
85
|
},
|
|
66
|
-
|
|
67
|
-
return
|
|
86
|
+
getRenderEvents(projectId, sessionId = null) {
|
|
87
|
+
return timelineRegistry.getEvents(routeKey(projectId, sessionId));
|
|
68
88
|
},
|
|
69
|
-
|
|
89
|
+
getRenderBlocks(projectId, sessionId = null) {
|
|
90
|
+
return timelineRegistry.getBlocks(routeKey(projectId, sessionId));
|
|
91
|
+
},
|
|
92
|
+
setRenderEvents(projectId, sessionId = null, events = []) {
|
|
70
93
|
const key = routeKey(projectId, sessionId);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
94
|
+
timelineRegistry.replaceEvents(key, events);
|
|
95
|
+
},
|
|
96
|
+
getRenderEventCount(projectId, sessionId = null) {
|
|
97
|
+
return timelineRegistry.getEventCount(routeKey(projectId, sessionId));
|
|
75
98
|
},
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
getRenderTimelineMetadata(projectId, sessionId = null) {
|
|
100
|
+
return timelineRegistry.getMetadata(routeKey(projectId, sessionId));
|
|
101
|
+
},
|
|
102
|
+
flushRenderTimeline(projectId, sessionId = null, reason = "manual") {
|
|
103
|
+
return timelineRegistry.flush(routeKey(projectId, sessionId), reason);
|
|
104
|
+
},
|
|
105
|
+
flushAllRenderTimelines(reason = "manual") {
|
|
106
|
+
return timelineRegistry.flushAll(reason);
|
|
78
107
|
},
|
|
79
108
|
};
|
|
80
109
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return current.sessionId == null && current.projectId === candidate.projectId;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function replayBufferedCall({ method, args }) {
|
|
89
|
-
const value = ui[method];
|
|
90
|
-
if (typeof value === "function") value.apply(ui, args);
|
|
110
|
+
function renderRoute(key) {
|
|
111
|
+
const timeline = timelineRegistry.ensure(key);
|
|
112
|
+
if (typeof ui.restoreTimelineBlocks !== "function") ui.clearOutput?.();
|
|
113
|
+
return timeline.replayTo(ui);
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
function recordRenderEvent(key, method, args) {
|
|
117
|
+
if (method === "clearOutput") {
|
|
118
|
+
timelineRegistry.clear(key);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const timeline = timelineRegistry.ensure(key);
|
|
122
|
+
timeline.apply(method, args);
|
|
123
|
+
if (PERSIST_FLUSH_METHODS.has(method)) timeline.flushPersist(method);
|
|
98
124
|
}
|
|
99
125
|
}
|
|
100
126
|
|
|
@@ -8,6 +8,7 @@ import { loadProjectLifecycleHookManifests } from "../../extensions/lifecycle-ma
|
|
|
8
8
|
import { loadKeybindings } from "../input/keybindings.mjs";
|
|
9
9
|
import { loadPromptTemplates } from "../input/prompt-templates.mjs";
|
|
10
10
|
import { loadOrCreateProjectId } from "../../workspace/project-id.mjs";
|
|
11
|
+
import { syncRuntimeSessionStateFromRunner } from "./runtime-session-state.mjs";
|
|
11
12
|
|
|
12
13
|
export async function createWorkspaceProjectRuntime({
|
|
13
14
|
project,
|
|
@@ -73,6 +74,7 @@ export async function createWorkspaceProjectRuntime({
|
|
|
73
74
|
refreshStatusBar,
|
|
74
75
|
onNotificationActivation,
|
|
75
76
|
});
|
|
77
|
+
syncRuntimeSessionStateFromRunner(sessionState, runner, sessionsRoot);
|
|
76
78
|
|
|
77
79
|
return {
|
|
78
80
|
project,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function syncRuntimeSessionStateFromRunner(sessionState, runner, sessionsRoot) {
|
|
4
|
+
const sessionId = runner?.getSessionStats?.()?.sessionId ?? null;
|
|
5
|
+
if (!sessionState || !sessionId) return null;
|
|
6
|
+
sessionState.sessionId = sessionId;
|
|
7
|
+
sessionState.sessionDir = sessionsRoot ? join(sessionsRoot, sessionId) : sessionState.sessionDir;
|
|
8
|
+
return sessionState;
|
|
9
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export function createTuiTimelineProjection() {
|
|
2
|
+
let blocks = [];
|
|
3
|
+
let openAssistantBlock = null;
|
|
4
|
+
let openThinkingBlock = null;
|
|
5
|
+
let openToolBlocks = [];
|
|
6
|
+
let nextBlockIndex = 1;
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
apply(event) {
|
|
10
|
+
applyProjectionEvent(event);
|
|
11
|
+
},
|
|
12
|
+
rebuild(events) {
|
|
13
|
+
resetProjection();
|
|
14
|
+
for (const event of events) applyProjectionEvent(event);
|
|
15
|
+
},
|
|
16
|
+
clear() {
|
|
17
|
+
resetProjection();
|
|
18
|
+
},
|
|
19
|
+
getBlocks() {
|
|
20
|
+
return blocks.map((block) => structuredCloneSafe(block));
|
|
21
|
+
},
|
|
22
|
+
getMetadata() {
|
|
23
|
+
return {
|
|
24
|
+
blockCount: blocks.length,
|
|
25
|
+
openAssistant: Boolean(openAssistantBlock),
|
|
26
|
+
openThinking: Boolean(openThinkingBlock),
|
|
27
|
+
openToolCount: openToolBlocks.length,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function applyProjectionEvent(event) {
|
|
33
|
+
const [first, second, third] = event.args ?? [];
|
|
34
|
+
switch (event.method) {
|
|
35
|
+
case "turnStart":
|
|
36
|
+
closeAssistantBlock();
|
|
37
|
+
blocks.push(createBlock("turn", event.at, { phase: "start" }));
|
|
38
|
+
break;
|
|
39
|
+
case "turnEnd":
|
|
40
|
+
closeAssistantBlock(event.at);
|
|
41
|
+
closeThinkingBlock(event.at);
|
|
42
|
+
blocks.push(createBlock("turn", event.at, { phase: "end" }));
|
|
43
|
+
break;
|
|
44
|
+
case "textDelta":
|
|
45
|
+
ensureAssistantBlock(event.at).content += String(first ?? "");
|
|
46
|
+
touchBlock(openAssistantBlock, event.at);
|
|
47
|
+
break;
|
|
48
|
+
case "assistantReplyEnd":
|
|
49
|
+
closeAssistantBlock(event.at);
|
|
50
|
+
break;
|
|
51
|
+
case "thinkingStart":
|
|
52
|
+
closeThinkingBlock(event.at);
|
|
53
|
+
openThinkingBlock = createBlock("thinking", event.at, { content: "", closed: false });
|
|
54
|
+
blocks.push(openThinkingBlock);
|
|
55
|
+
break;
|
|
56
|
+
case "thinkingDelta":
|
|
57
|
+
ensureThinkingBlock(event.at).content += String(first ?? "");
|
|
58
|
+
touchBlock(openThinkingBlock, event.at);
|
|
59
|
+
break;
|
|
60
|
+
case "thinkingEnd":
|
|
61
|
+
ensureThinkingBlock(event.at).tokens = first ?? null;
|
|
62
|
+
closeThinkingBlock(event.at);
|
|
63
|
+
break;
|
|
64
|
+
case "thinkingBlock":
|
|
65
|
+
closeThinkingBlock(event.at);
|
|
66
|
+
blocks.push(createBlock("thinking", event.at, { tokens: first ?? null, content: String(second ?? ""), closed: true }));
|
|
67
|
+
break;
|
|
68
|
+
case "toolStart": {
|
|
69
|
+
closeAssistantBlock(event.at);
|
|
70
|
+
const block = createBlock("tool", event.at, { name: first ?? null, args: second ?? null, result: null, isError: false, closed: false });
|
|
71
|
+
blocks.push(block);
|
|
72
|
+
openToolBlocks.push(block);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "toolEnd": {
|
|
76
|
+
const block = popOpenToolBlock(first);
|
|
77
|
+
if (block) {
|
|
78
|
+
block.name ??= first ?? null;
|
|
79
|
+
block.isError = Boolean(second);
|
|
80
|
+
block.result = third ?? null;
|
|
81
|
+
block.closed = true;
|
|
82
|
+
touchBlock(block, event.at);
|
|
83
|
+
} else {
|
|
84
|
+
blocks.push(createBlock("tool", event.at, { name: first ?? null, isError: Boolean(second), result: third ?? null, closed: true }));
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "write":
|
|
89
|
+
case "writeln":
|
|
90
|
+
closeAssistantBlock(event.at);
|
|
91
|
+
blocks.push(createBlock("output", event.at, { content: String(first ?? ""), newline: event.method === "writeln" }));
|
|
92
|
+
break;
|
|
93
|
+
case "status":
|
|
94
|
+
blocks.push(createBlock("status", event.at, { content: String(first ?? "") }));
|
|
95
|
+
break;
|
|
96
|
+
case "recall":
|
|
97
|
+
blocks.push(createBlock("recall", event.at, { hints: first?.hints ?? [] }));
|
|
98
|
+
break;
|
|
99
|
+
case "editDiff":
|
|
100
|
+
closeAssistantBlock(event.at);
|
|
101
|
+
blocks.push(createBlock("editDiff", event.at, { path: first ?? null, diffLines: second ?? [] }));
|
|
102
|
+
break;
|
|
103
|
+
case "retryStart":
|
|
104
|
+
case "retryEnd":
|
|
105
|
+
blocks.push(createBlock("retry", event.at, { method: event.method, payload: first ?? null }));
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
blocks.push(createBlock("event", event.at, { method: event.method, args: event.args ?? [] }));
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ensureAssistantBlock(at) {
|
|
114
|
+
if (!openAssistantBlock) {
|
|
115
|
+
openAssistantBlock = createBlock("assistant", at, { content: "", closed: false });
|
|
116
|
+
blocks.push(openAssistantBlock);
|
|
117
|
+
}
|
|
118
|
+
return openAssistantBlock;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function closeAssistantBlock(at = null) {
|
|
122
|
+
if (!openAssistantBlock) return;
|
|
123
|
+
openAssistantBlock.closed = true;
|
|
124
|
+
touchBlock(openAssistantBlock, at);
|
|
125
|
+
openAssistantBlock = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ensureThinkingBlock(at) {
|
|
129
|
+
if (!openThinkingBlock) {
|
|
130
|
+
openThinkingBlock = createBlock("thinking", at, { content: "", closed: false });
|
|
131
|
+
blocks.push(openThinkingBlock);
|
|
132
|
+
}
|
|
133
|
+
return openThinkingBlock;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function closeThinkingBlock(at = null) {
|
|
137
|
+
if (!openThinkingBlock) return;
|
|
138
|
+
openThinkingBlock.closed = true;
|
|
139
|
+
touchBlock(openThinkingBlock, at);
|
|
140
|
+
openThinkingBlock = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function popOpenToolBlock(name) {
|
|
144
|
+
if (openToolBlocks.length === 0) return null;
|
|
145
|
+
if (name == null) return openToolBlocks.pop();
|
|
146
|
+
const index = findLastIndex(openToolBlocks, (block) => block.name === name);
|
|
147
|
+
if (index < 0) return openToolBlocks.pop();
|
|
148
|
+
return openToolBlocks.splice(index, 1)[0];
|
|
149
|
+
}
|
|
150
|
+
function resetProjection() {
|
|
151
|
+
blocks = [];
|
|
152
|
+
openAssistantBlock = null;
|
|
153
|
+
openThinkingBlock = null;
|
|
154
|
+
openToolBlocks = [];
|
|
155
|
+
nextBlockIndex = 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createBlock(type, at, fields = {}) {
|
|
159
|
+
const id = `${type}-${nextBlockIndex++}`;
|
|
160
|
+
return { id, type, createdAt: at ?? null, updatedAt: at ?? null, ...fields };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function touchBlock(block, at = null) {
|
|
165
|
+
if (!block || at == null) return;
|
|
166
|
+
block.updatedAt = at;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function findLastIndex(items, predicate) {
|
|
170
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
171
|
+
if (predicate(items[index], index)) return index;
|
|
172
|
+
}
|
|
173
|
+
return -1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function structuredCloneSafe(value) {
|
|
177
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
178
|
+
return JSON.parse(JSON.stringify(value));
|
|
179
|
+
}
|