pi-ui-extend 0.1.9 → 0.1.13
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 +23 -2
- package/dist/app/app.d.ts +4 -0
- package/dist/app/app.js +76 -7
- package/dist/app/cli/install.d.ts +16 -0
- package/dist/app/cli/install.js +34 -7
- package/dist/app/cli/startup-info.js +5 -2
- package/dist/app/cli/update.d.ts +7 -0
- package/dist/app/cli/update.js +11 -3
- package/dist/app/commands/command-controller.js +4 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-model-actions.d.ts +5 -0
- package/dist/app/commands/command-model-actions.js +104 -0
- package/dist/app/commands/command-navigation-actions.d.ts +6 -1
- package/dist/app/commands/command-navigation-actions.js +37 -14
- package/dist/app/commands/command-registry.d.ts +4 -0
- package/dist/app/commands/command-registry.js +32 -0
- package/dist/app/commands/command-session-actions.d.ts +1 -0
- package/dist/app/commands/command-session-actions.js +15 -5
- package/dist/app/commands/shell-command.d.ts +7 -0
- package/dist/app/commands/shell-command.js +12 -4
- package/dist/app/commands/shell-controller.d.ts +1 -0
- package/dist/app/commands/shell-controller.js +1 -1
- package/dist/app/constants.d.ts +1 -1
- package/dist/app/constants.js +1 -1
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +3 -1
- package/dist/app/input/autocomplete-controller.d.ts +52 -0
- package/dist/app/input/autocomplete-controller.js +352 -0
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +21 -0
- package/dist/app/input/input-controller.d.ts +1 -0
- package/dist/app/input/input-controller.js +2 -0
- package/dist/app/input/input-paste-handler.d.ts +1 -0
- package/dist/app/input/input-paste-handler.js +22 -18
- package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
- package/dist/app/input/prompt-enhancer-controller.js +12 -3
- package/dist/app/input/voice-controller.d.ts +51 -1
- package/dist/app/input/voice-controller.js +42 -19
- package/dist/app/model/model-usage-status.d.ts +9 -0
- package/dist/app/model/model-usage-status.js +124 -34
- package/dist/app/popup/popup-action-controller.js +1 -1
- package/dist/app/process.d.ts +17 -0
- package/dist/app/process.js +68 -0
- package/dist/app/rendering/conversation-entry-renderer.js +8 -6
- package/dist/app/rendering/conversation-tool-renderer.js +3 -2
- package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
- package/dist/app/rendering/editor-layout-renderer.js +11 -1
- package/dist/app/rendering/message-content.js +65 -7
- package/dist/app/rendering/render-controller.js +6 -1
- package/dist/app/rendering/render-text.d.ts +3 -0
- package/dist/app/rendering/render-text.js +51 -3
- package/dist/app/rendering/status-line-renderer.d.ts +5 -1
- package/dist/app/rendering/status-line-renderer.js +61 -25
- package/dist/app/rendering/toast-renderer.js +10 -13
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
- package/dist/app/rendering/tool-block-renderer.js +16 -33
- package/dist/app/runtime.d.ts +6 -1
- package/dist/app/runtime.js +35 -2
- package/dist/app/screen/clipboard.d.ts +11 -2
- package/dist/app/screen/clipboard.js +29 -21
- package/dist/app/screen/file-link-opener.d.ts +8 -0
- package/dist/app/screen/file-link-opener.js +11 -3
- package/dist/app/screen/file-links.js +3 -3
- package/dist/app/screen/image-opener.d.ts +12 -0
- package/dist/app/screen/image-opener.js +13 -5
- package/dist/app/screen/mouse-controller.d.ts +5 -2
- package/dist/app/screen/mouse-controller.js +16 -1
- package/dist/app/screen/screen-styler.d.ts +4 -1
- package/dist/app/screen/screen-styler.js +3 -2
- package/dist/app/screen/status-controller.d.ts +3 -0
- package/dist/app/screen/status-controller.js +23 -8
- package/dist/app/session/queued-message-controller.d.ts +7 -1
- package/dist/app/session/queued-message-controller.js +36 -21
- package/dist/app/session/resume-session-loader.d.ts +15 -0
- package/dist/app/session/resume-session-loader.js +204 -0
- package/dist/app/session/session-event-controller.d.ts +5 -1
- package/dist/app/session/session-event-controller.js +72 -5
- package/dist/app/session/session-history.js +4 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
- package/dist/app/session/session-lifecycle-controller.js +9 -1
- package/dist/app/session/tabs-controller.d.ts +10 -1
- package/dist/app/session/tabs-controller.js +101 -5
- package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
- package/dist/app/terminal/nerd-font-controller.js +30 -23
- package/dist/app/terminal/terminal-controller.d.ts +1 -0
- package/dist/app/terminal/terminal-controller.js +1 -0
- package/dist/app/types.d.ts +14 -0
- package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
- package/dist/app/workspace/workspace-actions-controller.js +3 -3
- package/dist/app/workspace/workspace-undo.d.ts +1 -1
- package/dist/app/workspace/workspace-undo.js +22 -20
- package/dist/config.d.ts +27 -0
- package/dist/config.js +174 -1
- package/dist/default-pix-config.js +39 -353
- package/dist/input-editor.d.ts +7 -1
- package/dist/input-editor.js +47 -6
- package/dist/markdown-format.d.ts +1 -0
- package/dist/markdown-format.js +26 -1
- package/dist/schemas/index.d.ts +5 -0
- package/dist/schemas/index.js +5 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +177 -0
- package/dist/schemas/pi-tools-suite-schema.js +218 -0
- package/dist/schemas/pix-schema.d.ts +65 -0
- package/dist/schemas/pix-schema.js +91 -0
- package/dist/terminal-width.js +73 -56
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +46 -195
- package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
- package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
- package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
- package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
- package/external/pi-tools-suite/src/todo/index.ts +4 -2
- package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
- package/external/pi-tools-suite/src/todo/todo.ts +2 -6
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- package/package.json +12 -3
- package/schemas/pi-tools-suite.json +881 -0
- package/schemas/pix.json +298 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { open, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { isRecord } from "../guards.js";
|
|
5
|
+
const DEFAULT_INITIAL_CHUNK_SIZE = 20;
|
|
6
|
+
const DEFAULT_CHUNK_SIZE = 10;
|
|
7
|
+
export async function loadResumeSessionsInChunks(options) {
|
|
8
|
+
const initialChunkSize = positiveInteger(options.initialChunkSize, DEFAULT_INITIAL_CHUNK_SIZE);
|
|
9
|
+
const chunkSize = positiveInteger(options.chunkSize, DEFAULT_CHUNK_SIZE);
|
|
10
|
+
const files = await listSessionFiles(options.cwd, options.sessionDir);
|
|
11
|
+
const sessions = [];
|
|
12
|
+
if (files.length === 0) {
|
|
13
|
+
options.onChunk([], { loaded: 0, total: 0, done: true });
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
let index = 0;
|
|
17
|
+
let currentChunkSize = initialChunkSize;
|
|
18
|
+
while (index < files.length) {
|
|
19
|
+
if (options.signal?.aborted)
|
|
20
|
+
break;
|
|
21
|
+
const chunk = files.slice(index, index + currentChunkSize);
|
|
22
|
+
const infos = await Promise.all(chunk.map((file) => buildSessionInfo(file)));
|
|
23
|
+
for (const info of infos) {
|
|
24
|
+
if (info)
|
|
25
|
+
sessions.push(info);
|
|
26
|
+
}
|
|
27
|
+
sessions.sort(compareSessionsByModifiedDesc);
|
|
28
|
+
index += currentChunkSize;
|
|
29
|
+
const done = index >= files.length || options.signal?.aborted === true;
|
|
30
|
+
options.onChunk([...sessions], { loaded: Math.min(index, files.length), total: files.length, done });
|
|
31
|
+
currentChunkSize = chunkSize;
|
|
32
|
+
if (!done)
|
|
33
|
+
await nextTick();
|
|
34
|
+
}
|
|
35
|
+
return [...sessions];
|
|
36
|
+
}
|
|
37
|
+
async function listSessionFiles(cwd, sessionDir) {
|
|
38
|
+
const dir = sessionDir ? resolve(sessionDir) : getDefaultSessionDir(cwd);
|
|
39
|
+
const defaultDir = getDefaultSessionDir(cwd);
|
|
40
|
+
const shouldFilterCwd = sessionDir !== undefined && resolve(dir) !== resolve(defaultDir);
|
|
41
|
+
const resolvedCwd = resolve(cwd);
|
|
42
|
+
try {
|
|
43
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
44
|
+
const files = await Promise.all(entries
|
|
45
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
46
|
+
.map(async (entry) => {
|
|
47
|
+
const path = join(dir, entry.name);
|
|
48
|
+
try {
|
|
49
|
+
const stats = await stat(path);
|
|
50
|
+
if (shouldFilterCwd) {
|
|
51
|
+
const header = await readSessionHeader(path);
|
|
52
|
+
if (!header || !sessionCwdMatches(header.cwd, resolvedCwd))
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return { path, mtime: stats.mtime };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
return files
|
|
62
|
+
.filter((file) => file !== undefined)
|
|
63
|
+
.sort((left, right) => right.mtime.getTime() - left.mtime.getTime());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function readSessionHeader(filePath) {
|
|
70
|
+
try {
|
|
71
|
+
const handle = await open(filePath, "r");
|
|
72
|
+
try {
|
|
73
|
+
const buffer = Buffer.alloc(4096);
|
|
74
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
75
|
+
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
|
|
76
|
+
if (!firstLine)
|
|
77
|
+
return undefined;
|
|
78
|
+
const header = JSON.parse(firstLine);
|
|
79
|
+
if (!isRecord(header) || header.type !== "session" || typeof header.id !== "string")
|
|
80
|
+
return undefined;
|
|
81
|
+
return typeof header.cwd === "string" ? { id: header.id, cwd: header.cwd } : { id: header.id };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getDefaultSessionDir(cwd) {
|
|
92
|
+
const resolvedCwd = resolve(cwd);
|
|
93
|
+
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
94
|
+
return join(resolve(getAgentDir()), "sessions", safePath);
|
|
95
|
+
}
|
|
96
|
+
async function buildSessionInfo(file) {
|
|
97
|
+
try {
|
|
98
|
+
const content = await readFile(file.path, "utf8");
|
|
99
|
+
const entries = parseJsonLines(content);
|
|
100
|
+
const header = entries[0];
|
|
101
|
+
if (!isRecord(header) || header.type !== "session" || typeof header.id !== "string")
|
|
102
|
+
return undefined;
|
|
103
|
+
let messageCount = 0;
|
|
104
|
+
let firstMessage = "";
|
|
105
|
+
let name;
|
|
106
|
+
const allMessages = [];
|
|
107
|
+
let lastActivityTime;
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!isRecord(entry))
|
|
110
|
+
continue;
|
|
111
|
+
if (entry.type === "session_info") {
|
|
112
|
+
name = typeof entry.name === "string" ? entry.name.trim() || undefined : undefined;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.type !== "message" || !isRecord(entry.message))
|
|
116
|
+
continue;
|
|
117
|
+
messageCount++;
|
|
118
|
+
const message = entry.message;
|
|
119
|
+
const role = message.role;
|
|
120
|
+
if (role !== "user" && role !== "assistant")
|
|
121
|
+
continue;
|
|
122
|
+
const textContent = extractTextContent(message.content);
|
|
123
|
+
if (textContent) {
|
|
124
|
+
allMessages.push(textContent);
|
|
125
|
+
if (!firstMessage && role === "user")
|
|
126
|
+
firstMessage = textContent;
|
|
127
|
+
}
|
|
128
|
+
lastActivityTime = latestTimestamp(lastActivityTime, message.timestamp);
|
|
129
|
+
lastActivityTime = latestTimestamp(lastActivityTime, entry.timestamp);
|
|
130
|
+
}
|
|
131
|
+
const headerTimestamp = typeof header.timestamp === "string" ? new Date(header.timestamp).getTime() : NaN;
|
|
132
|
+
const modified = typeof lastActivityTime === "number"
|
|
133
|
+
? new Date(lastActivityTime)
|
|
134
|
+
: (!Number.isNaN(headerTimestamp) ? new Date(headerTimestamp) : file.mtime);
|
|
135
|
+
return {
|
|
136
|
+
path: file.path,
|
|
137
|
+
id: header.id,
|
|
138
|
+
cwd: typeof header.cwd === "string" ? header.cwd : "",
|
|
139
|
+
...(name === undefined ? {} : { name }),
|
|
140
|
+
...(typeof header.parentSession === "string" ? { parentSessionPath: header.parentSession } : {}),
|
|
141
|
+
created: new Date(typeof header.timestamp === "string" ? header.timestamp : file.mtime),
|
|
142
|
+
modified,
|
|
143
|
+
messageCount,
|
|
144
|
+
firstMessage: firstMessage || "(no messages)",
|
|
145
|
+
allMessagesText: allMessages.join(" "),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function parseJsonLines(content) {
|
|
153
|
+
const entries = [];
|
|
154
|
+
for (const line of content.trim().split("\n")) {
|
|
155
|
+
if (!line.trim())
|
|
156
|
+
continue;
|
|
157
|
+
try {
|
|
158
|
+
entries.push(JSON.parse(line));
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Ignore malformed lines, matching SDK session listing behavior.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return entries;
|
|
165
|
+
}
|
|
166
|
+
function extractTextContent(content) {
|
|
167
|
+
if (typeof content === "string")
|
|
168
|
+
return content;
|
|
169
|
+
if (!Array.isArray(content))
|
|
170
|
+
return "";
|
|
171
|
+
return content
|
|
172
|
+
.map((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" ? block.text : "")
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(" ");
|
|
175
|
+
}
|
|
176
|
+
function latestTimestamp(current, value) {
|
|
177
|
+
let timestamp;
|
|
178
|
+
if (typeof value === "number")
|
|
179
|
+
timestamp = value;
|
|
180
|
+
else if (typeof value === "string") {
|
|
181
|
+
const parsed = new Date(value).getTime();
|
|
182
|
+
if (!Number.isNaN(parsed))
|
|
183
|
+
timestamp = parsed;
|
|
184
|
+
}
|
|
185
|
+
if (timestamp === undefined)
|
|
186
|
+
return current;
|
|
187
|
+
return Math.max(current ?? 0, timestamp);
|
|
188
|
+
}
|
|
189
|
+
function sessionCwdMatches(cwd, resolvedCwd) {
|
|
190
|
+
return cwd !== undefined && cwd !== "" && resolve(cwd) === resolvedCwd;
|
|
191
|
+
}
|
|
192
|
+
function compareSessionsByModifiedDesc(left, right) {
|
|
193
|
+
return right.modified.getTime() - left.modified.getTime();
|
|
194
|
+
}
|
|
195
|
+
function positiveInteger(value, fallback) {
|
|
196
|
+
if (value === undefined || !Number.isFinite(value) || value < 1)
|
|
197
|
+
return fallback;
|
|
198
|
+
return Math.floor(value);
|
|
199
|
+
}
|
|
200
|
+
function nextTick() {
|
|
201
|
+
return new Promise((resolveTick) => {
|
|
202
|
+
setImmediate(resolveTick);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -8,11 +8,11 @@ export type AppSessionEventControllerHost = {
|
|
|
8
8
|
conversationViewport(): ConversationViewport;
|
|
9
9
|
isRunning(): boolean;
|
|
10
10
|
render(): void;
|
|
11
|
+
scheduleRender(): void;
|
|
11
12
|
setStatus(status: string): void;
|
|
12
13
|
restoreSessionStatus(): void;
|
|
13
14
|
setSessionStatus(session: AgentSessionRuntime["session"] | undefined): void;
|
|
14
15
|
setSessionActivity(activity: SessionActivity): void;
|
|
15
|
-
flushDeferredUserMessages(): void;
|
|
16
16
|
updateQueuedMessageStatus(): void;
|
|
17
17
|
prepareWorkspaceMutation(toolName: string, args: unknown): WorkspaceMutationPreparation | undefined;
|
|
18
18
|
recordWorkspaceMutationForUserEntry(entryId: string, mutation: WorkspaceMutation): void;
|
|
@@ -39,6 +39,7 @@ export declare class AppSessionEventController {
|
|
|
39
39
|
private currentUserEntryId;
|
|
40
40
|
private currentAssistantEntryId;
|
|
41
41
|
private currentThinkingEntryId;
|
|
42
|
+
private assistantTextBuffer;
|
|
42
43
|
constructor(host: AppSessionEventControllerHost);
|
|
43
44
|
reset(): void;
|
|
44
45
|
loadSessionHistory(): void;
|
|
@@ -62,6 +63,9 @@ export declare class AppSessionEventController {
|
|
|
62
63
|
private recordToolWorkspaceMutation;
|
|
63
64
|
private handleMessageUpdate;
|
|
64
65
|
private appendAssistantText;
|
|
66
|
+
private flushAssistantTextBuffer;
|
|
67
|
+
private drainAssistantTextBuffer;
|
|
68
|
+
private hasVisibleAssistantText;
|
|
65
69
|
private appendThinkingText;
|
|
66
70
|
private finishCurrentThinkingEntry;
|
|
67
71
|
private upsertToolEntry;
|
|
@@ -2,6 +2,8 @@ import { createId } from "../id.js";
|
|
|
2
2
|
import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
|
|
3
3
|
import { customMessageEntry, loadSessionHistoryEntries, loadSessionHistoryEntriesAsync } from "./session-history.js";
|
|
4
4
|
import { isRecord } from "../guards.js";
|
|
5
|
+
const DCP_MESSAGE_REFERENCE_PREFIX = "[dcp-id]: # (m";
|
|
6
|
+
const DCP_BLOCK_REFERENCE_PREFIX = "[dcp-block-id]: # (b";
|
|
5
7
|
export class AppSessionEventController {
|
|
6
8
|
host;
|
|
7
9
|
entryRenderVersions = new Map();
|
|
@@ -10,6 +12,7 @@ export class AppSessionEventController {
|
|
|
10
12
|
currentUserEntryId;
|
|
11
13
|
currentAssistantEntryId;
|
|
12
14
|
currentThinkingEntryId;
|
|
15
|
+
assistantTextBuffer = "";
|
|
13
16
|
constructor(host) {
|
|
14
17
|
this.host = host;
|
|
15
18
|
}
|
|
@@ -20,6 +23,7 @@ export class AppSessionEventController {
|
|
|
20
23
|
this.entryRenderVersions.clear();
|
|
21
24
|
this.currentAssistantEntryId = undefined;
|
|
22
25
|
this.currentThinkingEntryId = undefined;
|
|
26
|
+
this.assistantTextBuffer = "";
|
|
23
27
|
}
|
|
24
28
|
loadSessionHistory() {
|
|
25
29
|
const runtime = this.host.runtime();
|
|
@@ -64,7 +68,6 @@ export class AppSessionEventController {
|
|
|
64
68
|
case "agent_start":
|
|
65
69
|
this.host.setSessionActivity("running");
|
|
66
70
|
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
67
|
-
this.host.flushDeferredUserMessages();
|
|
68
71
|
break;
|
|
69
72
|
case "thinking_level_changed":
|
|
70
73
|
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
@@ -75,7 +78,6 @@ export class AppSessionEventController {
|
|
|
75
78
|
this.currentUserEntryId = undefined;
|
|
76
79
|
this.host.setSessionActivity("idle");
|
|
77
80
|
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
78
|
-
this.host.flushDeferredUserMessages();
|
|
79
81
|
break;
|
|
80
82
|
case "queue_update":
|
|
81
83
|
this.host.updateQueuedMessageStatus();
|
|
@@ -85,6 +87,7 @@ export class AppSessionEventController {
|
|
|
85
87
|
break;
|
|
86
88
|
case "tool_execution_start":
|
|
87
89
|
this.finishCurrentThinkingEntry();
|
|
90
|
+
this.flushAssistantTextBuffer(true);
|
|
88
91
|
this.currentAssistantEntryId = undefined;
|
|
89
92
|
this.host.setSessionActivity("running");
|
|
90
93
|
this.prepareToolWorkspaceMutation(event.toolCallId, event.toolName, event.args);
|
|
@@ -134,7 +137,6 @@ export class AppSessionEventController {
|
|
|
134
137
|
? "Compaction cancelled"
|
|
135
138
|
: event.errorMessage ?? "Compaction failed";
|
|
136
139
|
this.host.showToast(message, event.result ? "success" : event.aborted ? "info" : "error");
|
|
137
|
-
this.host.flushDeferredUserMessages();
|
|
138
140
|
break;
|
|
139
141
|
}
|
|
140
142
|
case "auto_retry_start":
|
|
@@ -149,7 +151,7 @@ export class AppSessionEventController {
|
|
|
149
151
|
default:
|
|
150
152
|
break;
|
|
151
153
|
}
|
|
152
|
-
this.host.
|
|
154
|
+
this.host.scheduleRender();
|
|
153
155
|
}
|
|
154
156
|
addCustomMessageEntry(message) {
|
|
155
157
|
const entry = customMessageEntry(message);
|
|
@@ -214,6 +216,7 @@ export class AppSessionEventController {
|
|
|
214
216
|
}
|
|
215
217
|
if (isRecord(message) && message.role === "assistant") {
|
|
216
218
|
this.finishCurrentThinkingEntry();
|
|
219
|
+
this.flushAssistantTextBuffer(true);
|
|
217
220
|
this.clearCurrentAssistantState();
|
|
218
221
|
this.currentUserEntryId = undefined;
|
|
219
222
|
}
|
|
@@ -258,11 +261,13 @@ export class AppSessionEventController {
|
|
|
258
261
|
break;
|
|
259
262
|
case "done":
|
|
260
263
|
this.finishCurrentThinkingEntry();
|
|
264
|
+
this.flushAssistantTextBuffer(true);
|
|
261
265
|
this.clearCurrentAssistantState();
|
|
262
266
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
263
267
|
break;
|
|
264
268
|
case "error":
|
|
265
269
|
this.finishCurrentThinkingEntry();
|
|
270
|
+
this.flushAssistantTextBuffer(true);
|
|
266
271
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
267
272
|
this.addEntry({ id: createId("error"), kind: "error", text: assistantEvent.error.errorMessage ?? assistantEvent.reason });
|
|
268
273
|
break;
|
|
@@ -271,15 +276,51 @@ export class AppSessionEventController {
|
|
|
271
276
|
}
|
|
272
277
|
}
|
|
273
278
|
appendAssistantText(delta) {
|
|
279
|
+
this.assistantTextBuffer += delta;
|
|
280
|
+
this.flushAssistantTextBuffer(false);
|
|
281
|
+
}
|
|
282
|
+
flushAssistantTextBuffer(final) {
|
|
283
|
+
const visibleText = this.drainAssistantTextBuffer(final);
|
|
284
|
+
if (!visibleText)
|
|
285
|
+
return;
|
|
274
286
|
let entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
|
|
275
287
|
if (!entry || entry.kind !== "assistant") {
|
|
276
288
|
entry = { id: createId("assistant"), kind: "assistant", text: "" };
|
|
277
289
|
this.addEntry(entry);
|
|
278
290
|
this.currentAssistantEntryId = entry.id;
|
|
279
291
|
}
|
|
280
|
-
entry.text +=
|
|
292
|
+
entry.text += visibleText;
|
|
281
293
|
this.touchEntry(entry);
|
|
282
294
|
}
|
|
295
|
+
drainAssistantTextBuffer(final) {
|
|
296
|
+
let visibleText = "";
|
|
297
|
+
for (;;) {
|
|
298
|
+
const newlineIndex = this.assistantTextBuffer.indexOf("\n");
|
|
299
|
+
if (newlineIndex === -1)
|
|
300
|
+
break;
|
|
301
|
+
const line = this.assistantTextBuffer.slice(0, newlineIndex);
|
|
302
|
+
this.assistantTextBuffer = this.assistantTextBuffer.slice(newlineIndex + 1);
|
|
303
|
+
if (shouldDropAssistantStreamLine(line, this.hasVisibleAssistantText(visibleText)))
|
|
304
|
+
continue;
|
|
305
|
+
visibleText += `${line}\n`;
|
|
306
|
+
}
|
|
307
|
+
if (!this.assistantTextBuffer)
|
|
308
|
+
return visibleText;
|
|
309
|
+
if (shouldHoldAssistantStreamTail(this.assistantTextBuffer)) {
|
|
310
|
+
if (final)
|
|
311
|
+
this.assistantTextBuffer = "";
|
|
312
|
+
return visibleText;
|
|
313
|
+
}
|
|
314
|
+
visibleText += this.assistantTextBuffer;
|
|
315
|
+
this.assistantTextBuffer = "";
|
|
316
|
+
return visibleText;
|
|
317
|
+
}
|
|
318
|
+
hasVisibleAssistantText(pendingVisibleText) {
|
|
319
|
+
if (pendingVisibleText.length > 0)
|
|
320
|
+
return true;
|
|
321
|
+
const entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
|
|
322
|
+
return entry?.kind === "assistant" && entry.text.length > 0;
|
|
323
|
+
}
|
|
283
324
|
appendThinkingText(delta) {
|
|
284
325
|
let entry = this.currentThinkingEntryId ? this.findEntry(this.currentThinkingEntryId) : undefined;
|
|
285
326
|
if (!entry || entry.kind !== "thinking") {
|
|
@@ -334,5 +375,31 @@ export class AppSessionEventController {
|
|
|
334
375
|
clearCurrentAssistantState() {
|
|
335
376
|
this.currentAssistantEntryId = undefined;
|
|
336
377
|
this.currentThinkingEntryId = undefined;
|
|
378
|
+
this.assistantTextBuffer = "";
|
|
337
379
|
}
|
|
338
380
|
}
|
|
381
|
+
function shouldDropAssistantStreamLine(line, hasVisibleText) {
|
|
382
|
+
if (line.trim().length === 0 && !hasVisibleText)
|
|
383
|
+
return true;
|
|
384
|
+
return isHiddenMarkdownMetadataLine(line);
|
|
385
|
+
}
|
|
386
|
+
function shouldHoldAssistantStreamTail(text) {
|
|
387
|
+
if (text.trim().length === 0)
|
|
388
|
+
return true;
|
|
389
|
+
return isPotentialDcpMetadataLine(text);
|
|
390
|
+
}
|
|
391
|
+
function isHiddenMarkdownMetadataLine(line) {
|
|
392
|
+
return isMarkdownReferenceDefinition(line) || isPotentialDcpMetadataLine(line);
|
|
393
|
+
}
|
|
394
|
+
function isMarkdownReferenceDefinition(line) {
|
|
395
|
+
return /^ {0,3}\[[^\]\n]+\]:[ \t]*\S.*$/u.test(line);
|
|
396
|
+
}
|
|
397
|
+
function isPotentialDcpMetadataLine(line) {
|
|
398
|
+
const content = line.replace(/^ {0,3}/u, "");
|
|
399
|
+
if (content.length === 0)
|
|
400
|
+
return false;
|
|
401
|
+
return isPotentialDcpReference(content, DCP_MESSAGE_REFERENCE_PREFIX) || isPotentialDcpReference(content, DCP_BLOCK_REFERENCE_PREFIX);
|
|
402
|
+
}
|
|
403
|
+
function isPotentialDcpReference(content, markerPrefix) {
|
|
404
|
+
return markerPrefix.startsWith(content) || (content.startsWith(markerPrefix) && /^\d*\)?$/u.test(content.slice(markerPrefix.length)));
|
|
405
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRecord } from "../guards.js";
|
|
2
2
|
import { createId } from "../id.js";
|
|
3
|
+
import { isOnlyHiddenMetadata } from "../../markdown-format.js";
|
|
3
4
|
import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
|
|
4
5
|
const SYSTEM_CUSTOM_MESSAGE_TYPE = "pix-system";
|
|
5
6
|
const HISTORICAL_SUBAGENTS_OBSERVATION = { showSnapshot: false };
|
|
@@ -119,10 +120,10 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
119
120
|
options.addEntry({ id: createId("thinking"), kind: "thinking", text: thinkingText, expanded: false, status: "done" });
|
|
120
121
|
thinkingText = "";
|
|
121
122
|
}
|
|
122
|
-
if (assistantText) {
|
|
123
|
+
if (assistantText && !isOnlyHiddenMetadata(assistantText)) {
|
|
123
124
|
options.addEntry({ id: createId("assistant"), kind: "assistant", text: assistantText });
|
|
124
|
-
assistantText = "";
|
|
125
125
|
}
|
|
126
|
+
assistantText = "";
|
|
126
127
|
const toolCallId = String(block.id ?? createId("tool"));
|
|
127
128
|
const result = toolResults.get(toolCallId);
|
|
128
129
|
const toolName = result?.toolName ?? String(block.name ?? "unknown");
|
|
@@ -158,7 +159,7 @@ function renderAssistantHistoryMessage(message, toolResults, options) {
|
|
|
158
159
|
if (thinkingText) {
|
|
159
160
|
options.addEntry({ id: createId("thinking"), kind: "thinking", text: thinkingText, expanded: false, status: "done" });
|
|
160
161
|
}
|
|
161
|
-
if (assistantText) {
|
|
162
|
+
if (assistantText && !isOnlyHiddenMetadata(assistantText)) {
|
|
162
163
|
options.addEntry({ id: createId("assistant"), kind: "assistant", text: assistantText });
|
|
163
164
|
}
|
|
164
165
|
}
|
|
@@ -37,6 +37,10 @@ export type AppSessionLifecycleHost = {
|
|
|
37
37
|
clearMouseRenderState(): void;
|
|
38
38
|
scrollReset(): void;
|
|
39
39
|
loadSessionHistoryEntries(): void;
|
|
40
|
+
loadSessionHistoryEntriesAsync(options: {
|
|
41
|
+
isCancelled: () => boolean;
|
|
42
|
+
render: () => void;
|
|
43
|
+
}): Promise<boolean>;
|
|
40
44
|
syncUserSessionEntryMetadata(): void;
|
|
41
45
|
restoreTabsAfterStartup(): Promise<void>;
|
|
42
46
|
render(): void;
|
|
@@ -49,6 +53,7 @@ export declare class AppSessionLifecycleController {
|
|
|
49
53
|
bindCurrentSession(): Promise<void>;
|
|
50
54
|
unsubscribeSession(): void;
|
|
51
55
|
afterSessionReplacement(message?: string): void;
|
|
56
|
+
private loadReplacementHistory;
|
|
52
57
|
resetSessionView(): void;
|
|
53
58
|
loadSessionHistory(): void;
|
|
54
59
|
requireRuntime(): AgentSessionRuntime;
|
|
@@ -84,7 +84,15 @@ export class AppSessionLifecycleController {
|
|
|
84
84
|
}
|
|
85
85
|
afterSessionReplacement(message) {
|
|
86
86
|
this.resetSessionView();
|
|
87
|
-
this.
|
|
87
|
+
void this.loadReplacementHistory(message);
|
|
88
|
+
this.host.render();
|
|
89
|
+
}
|
|
90
|
+
async loadReplacementHistory(message) {
|
|
91
|
+
await this.host.loadSessionHistoryEntriesAsync({
|
|
92
|
+
isCancelled: () => !this.host.isRunning(),
|
|
93
|
+
render: () => this.host.render(),
|
|
94
|
+
});
|
|
95
|
+
this.host.syncUserSessionEntryMetadata();
|
|
88
96
|
if (message)
|
|
89
97
|
this.host.addEntry({ id: createId("system"), kind: "system", text: message });
|
|
90
98
|
const session = this.host.runtime()?.session;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentSession, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AppBlinkController } from "../screen/blink-controller.js";
|
|
3
|
-
import type { AppOptions, Entry, SessionActivity, SessionTab } from "../types.js";
|
|
3
|
+
import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessage } from "../types.js";
|
|
4
4
|
export type TabInputState = {
|
|
5
5
|
text: string;
|
|
6
6
|
cursor: number;
|
|
@@ -26,6 +26,8 @@ export type AppTabsControllerHost = {
|
|
|
26
26
|
syncUserSessionEntryMetadata(): void;
|
|
27
27
|
captureInputState(): TabInputState;
|
|
28
28
|
restoreInputState(state: TabInputState): void;
|
|
29
|
+
captureDeferredUserMessages?(): readonly SubmittedUserMessage[];
|
|
30
|
+
restoreDeferredUserMessages?(messages: readonly SubmittedUserMessage[]): void;
|
|
29
31
|
addEntry(entry: Entry): void;
|
|
30
32
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
31
33
|
render(): void;
|
|
@@ -36,6 +38,7 @@ export declare class AppTabsController {
|
|
|
36
38
|
private readonly runtimesByTabId;
|
|
37
39
|
private readonly runtimeSubscriptionsByTabId;
|
|
38
40
|
private readonly inputStatesByTabId;
|
|
41
|
+
private readonly deferredUserMessagesByTabId;
|
|
39
42
|
private activeTabId;
|
|
40
43
|
private pendingActiveTabId;
|
|
41
44
|
private historyLoadGeneration;
|
|
@@ -50,6 +53,7 @@ export declare class AppTabsController {
|
|
|
50
53
|
setInputStateForTab(tabId: string | undefined, state: TabInputState): Promise<void>;
|
|
51
54
|
disposeInactiveRuntimes(disposeRuntime?: (runtime: AgentSessionRuntime) => Promise<void>): Promise<void>;
|
|
52
55
|
saveInputStateForQuit(): Promise<void>;
|
|
56
|
+
persistActiveDeferredUserMessages(): void;
|
|
53
57
|
syncActiveTabFromRuntime(options?: {
|
|
54
58
|
save?: boolean;
|
|
55
59
|
force?: boolean;
|
|
@@ -74,13 +78,17 @@ export declare class AppTabsController {
|
|
|
74
78
|
private shouldSyncTabFromRuntimeEvent;
|
|
75
79
|
private syncTabFromObservedRuntime;
|
|
76
80
|
private storeActiveInputState;
|
|
81
|
+
private storeActiveDeferredUserMessages;
|
|
77
82
|
private restoreInputState;
|
|
83
|
+
private restoreDeferredUserMessages;
|
|
84
|
+
private cloneSubmittedUserMessage;
|
|
78
85
|
private runtimeForTab;
|
|
79
86
|
private findTabForSession;
|
|
80
87
|
private findTabBySessionPath;
|
|
81
88
|
private replaceTabs;
|
|
82
89
|
private removeTab;
|
|
83
90
|
private restorePersistedInputStates;
|
|
91
|
+
private restorePersistedDeferredUserMessages;
|
|
84
92
|
private ensureCurrentSessionTab;
|
|
85
93
|
private tabFromSession;
|
|
86
94
|
private updateTabFromSession;
|
|
@@ -95,6 +103,7 @@ export declare class AppTabsController {
|
|
|
95
103
|
private restoredTabs;
|
|
96
104
|
private loadTabs;
|
|
97
105
|
private parsePersistedInputState;
|
|
106
|
+
private parsePersistedSubmittedUserMessages;
|
|
98
107
|
private saveTabs;
|
|
99
108
|
private filePath;
|
|
100
109
|
}
|