@pimote/pimote 0.1.1 → 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/README.md +3 -1
- package/client/build/_app/immutable/assets/0.DBrr7n4n.css +2 -0
- package/client/build/_app/immutable/assets/2.DE6k3bQj.css +1 -0
- package/client/build/_app/immutable/chunks/5vSSf6qG.js +5 -0
- package/client/build/_app/immutable/chunks/{BTSGQ0LP.js → B8lQCytv.js} +1 -1
- package/client/build/_app/immutable/chunks/{BEKHoMUP.js → CT6ckxpD.js} +1 -1
- package/client/build/_app/immutable/chunks/DlJOVoUQ.js +1 -0
- package/client/build/_app/immutable/chunks/YxmLwfhj.js +1 -0
- package/client/build/_app/immutable/chunks/{L5t1qIFa.js → uZO1iyJZ.js} +2 -2
- package/client/build/_app/immutable/chunks/yWVx3W2o.js +1 -0
- package/client/build/_app/immutable/entry/app.CNzpBgAg.js +2 -0
- package/client/build/_app/immutable/entry/start.DYkTAHh1.js +1 -0
- package/client/build/_app/immutable/nodes/0.DNlQhEb_.js +10 -0
- package/client/build/_app/immutable/nodes/1.B8zmHMre.js +1 -0
- package/client/build/_app/immutable/nodes/2.W9yV4-x2.js +54 -0
- package/client/build/_app/version.json +1 -1
- package/client/build/index.html +8 -8
- package/package.json +3 -3
- package/patches/{@mariozechner+pi-coding-agent+0.65.0.patch → @mariozechner+pi-coding-agent+0.67.6.patch} +4 -4
- package/server/dist/folder-index.js +8 -4
- package/server/dist/git-branch.js +32 -0
- package/server/dist/message-mapper.js +65 -2
- package/server/dist/server.js +3 -0
- package/server/dist/session-manager.js +35 -4
- package/server/dist/ws-handler.js +232 -31
- package/client/build/_app/immutable/assets/0.Cj7UL9cq.css +0 -2
- package/client/build/_app/immutable/assets/2.CIRqqeIr.css +0 -1
- package/client/build/_app/immutable/chunks/CfQ6Egqh.js +0 -1
- package/client/build/_app/immutable/chunks/DQ-KfPq0.js +0 -1
- package/client/build/_app/immutable/chunks/DfA0ecbz.js +0 -1
- package/client/build/_app/immutable/chunks/Dnh9Emns.js +0 -5
- package/client/build/_app/immutable/entry/app.j0V4R67V.js +0 -2
- package/client/build/_app/immutable/entry/start.wkfo4Ebw.js +0 -1
- package/client/build/_app/immutable/nodes/0.CUipL_P7.js +0 -5
- package/client/build/_app/immutable/nodes/1.ex7ejMby.js +0 -1
- package/client/build/_app/immutable/nodes/2.165oQG9Z.js +0 -49
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAgentSessionRuntime, createAgentSessionServices, createAgentSessionFromServices, createEventBus, AuthStorage, ModelRegistry, getAgentDir, SessionManager as PiSessionManager, } from '@mariozechner/pi-coding-agent';
|
|
2
2
|
import { EventBuffer } from './event-buffer.js';
|
|
3
3
|
import { applyPanelMessage, getMergedPanelCards } from './panel-state.js';
|
|
4
|
+
import { getGitBranch } from './git-branch.js';
|
|
4
5
|
// ---- Slot-based helpers (operate on ManagedSlot) ----
|
|
5
6
|
/** Send an event to the client connected to this slot. No-op if disconnected. */
|
|
6
7
|
export function sendSlotEvent(slot, event) {
|
|
@@ -59,6 +60,7 @@ function createSessionState(session, eventBus, config, callbacks, slotRef, folde
|
|
|
59
60
|
panelState: new Map(),
|
|
60
61
|
panelListenerUnsubs: [],
|
|
61
62
|
panelThrottleTimer: null,
|
|
63
|
+
treeNavigationInProgress: false,
|
|
62
64
|
};
|
|
63
65
|
// Subscribe to session events
|
|
64
66
|
const unsubscribe = session.subscribe((event) => {
|
|
@@ -125,8 +127,11 @@ export class PimoteSessionManager {
|
|
|
125
127
|
modelRegistry;
|
|
126
128
|
sessions = new Map();
|
|
127
129
|
idleCheckHandle = null;
|
|
130
|
+
gitBranchCheckHandle = null;
|
|
131
|
+
lastKnownGitBranchBySession = new Map();
|
|
128
132
|
onStatusChange;
|
|
129
133
|
onSessionClosed;
|
|
134
|
+
onGitBranchChange;
|
|
130
135
|
constructor(config, pushNotificationService) {
|
|
131
136
|
this.config = config;
|
|
132
137
|
this.pushNotificationService = pushNotificationService;
|
|
@@ -137,6 +142,8 @@ export class PimoteSessionManager {
|
|
|
137
142
|
const eventBusRef = { current: null };
|
|
138
143
|
const sharedAuthStorage = this.authStorage;
|
|
139
144
|
const sharedModelRegistry = this.modelRegistry;
|
|
145
|
+
const sessionManager = sessionFilePath ? PiSessionManager.open(sessionFilePath) : PiSessionManager.create(folderPath);
|
|
146
|
+
const effectiveFolderPath = sessionFilePath ? sessionManager.getCwd() : folderPath;
|
|
140
147
|
const factory = async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
141
148
|
const eventBus = createEventBus();
|
|
142
149
|
eventBusRef.current = eventBus;
|
|
@@ -154,9 +161,9 @@ export class PimoteSessionManager {
|
|
|
154
161
|
};
|
|
155
162
|
};
|
|
156
163
|
const runtime = await createAgentSessionRuntime(factory, {
|
|
157
|
-
cwd:
|
|
164
|
+
cwd: effectiveFolderPath,
|
|
158
165
|
agentDir: getAgentDir(),
|
|
159
|
-
sessionManager
|
|
166
|
+
sessionManager,
|
|
160
167
|
});
|
|
161
168
|
const session = runtime.session;
|
|
162
169
|
const sessionId = session.sessionId;
|
|
@@ -186,10 +193,10 @@ export class PimoteSessionManager {
|
|
|
186
193
|
onStatusChange: (sid, fp) => this.onStatusChange?.(sid, fp),
|
|
187
194
|
onAgentEnd: (sid, s) => this.handleAgentEnd(sid, s),
|
|
188
195
|
sendEvent: (e) => sendSlotEvent(slot, e),
|
|
189
|
-
}, slotRef,
|
|
196
|
+
}, slotRef, effectiveFolderPath);
|
|
190
197
|
const slot = {
|
|
191
198
|
runtime,
|
|
192
|
-
folderPath,
|
|
199
|
+
folderPath: effectiveFolderPath,
|
|
193
200
|
eventBusRef,
|
|
194
201
|
connection: null,
|
|
195
202
|
sessionState,
|
|
@@ -199,6 +206,7 @@ export class PimoteSessionManager {
|
|
|
199
206
|
};
|
|
200
207
|
slotRef.slot = slot;
|
|
201
208
|
this.sessions.set(sessionId, slot);
|
|
209
|
+
this.lastKnownGitBranchBySession.set(sessionId, getGitBranch(effectiveFolderPath));
|
|
202
210
|
return sessionId;
|
|
203
211
|
}
|
|
204
212
|
handleAgentEnd(sessionId, slot) {
|
|
@@ -261,12 +269,16 @@ export class PimoteSessionManager {
|
|
|
261
269
|
const folderPath = slot.folderPath;
|
|
262
270
|
await slot.runtime.dispose();
|
|
263
271
|
this.sessions.delete(sessionId);
|
|
272
|
+
this.lastKnownGitBranchBySession.delete(sessionId);
|
|
264
273
|
this.onSessionClosed?.(sessionId, folderPath);
|
|
265
274
|
}
|
|
266
275
|
/** Re-key a slot in the session map after session replacement. */
|
|
267
276
|
reKeySession(slot, oldId, newId) {
|
|
268
277
|
this.sessions.delete(oldId);
|
|
269
278
|
this.sessions.set(newId, slot);
|
|
279
|
+
const lastKnown = this.lastKnownGitBranchBySession.get(oldId) ?? null;
|
|
280
|
+
this.lastKnownGitBranchBySession.delete(oldId);
|
|
281
|
+
this.lastKnownGitBranchBySession.set(newId, lastKnown);
|
|
270
282
|
}
|
|
271
283
|
/** Rebuild a slot's SessionState after session replacement.
|
|
272
284
|
* Tears down the old state and creates a new one from the current runtime.session. */
|
|
@@ -289,6 +301,9 @@ export class PimoteSessionManager {
|
|
|
289
301
|
this.stopIdleCheck();
|
|
290
302
|
this.idleCheckHandle = setInterval(() => {
|
|
291
303
|
for (const [sessionId, slot] of this.sessions) {
|
|
304
|
+
if (slot.sessionState.treeNavigationInProgress) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
292
307
|
const clientId = slot.connection?.connectedClientId ?? null;
|
|
293
308
|
const hasConnectedClient = clientId !== null && (isClientConnected?.(clientId) ?? false);
|
|
294
309
|
if (!hasConnectedClient && Date.now() - slot.sessionState.lastActivity > idleTimeout) {
|
|
@@ -298,12 +313,28 @@ export class PimoteSessionManager {
|
|
|
298
313
|
}
|
|
299
314
|
}
|
|
300
315
|
}, 60_000);
|
|
316
|
+
this.gitBranchCheckHandle = setInterval(() => {
|
|
317
|
+
for (const [sessionId, slot] of this.sessions) {
|
|
318
|
+
if (!slot.connection?.connectedClientId)
|
|
319
|
+
continue;
|
|
320
|
+
const next = getGitBranch(slot.folderPath);
|
|
321
|
+
const prev = this.lastKnownGitBranchBySession.get(sessionId) ?? null;
|
|
322
|
+
if (next !== prev) {
|
|
323
|
+
this.lastKnownGitBranchBySession.set(sessionId, next);
|
|
324
|
+
this.onGitBranchChange?.(sessionId, slot.folderPath);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}, 3000);
|
|
301
328
|
}
|
|
302
329
|
stopIdleCheck() {
|
|
303
330
|
if (this.idleCheckHandle !== null) {
|
|
304
331
|
clearInterval(this.idleCheckHandle);
|
|
305
332
|
this.idleCheckHandle = null;
|
|
306
333
|
}
|
|
334
|
+
if (this.gitBranchCheckHandle !== null) {
|
|
335
|
+
clearInterval(this.gitBranchCheckHandle);
|
|
336
|
+
this.gitBranchCheckHandle = null;
|
|
337
|
+
}
|
|
307
338
|
}
|
|
308
339
|
async dispose() {
|
|
309
340
|
this.stopIdleCheck();
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { join, sep } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
2
5
|
import { resolveAllSlotPendingUi, resolveSlotPendingUi, replaySlotPendingUiRequests } from './session-manager.js';
|
|
3
6
|
import { getMergedPanelCards } from './panel-state.js';
|
|
4
7
|
import { createExtensionUIBridge } from './extension-ui-bridge.js';
|
|
5
8
|
import { findExternalPiProcesses, killExternalPiProcesses } from './takeover.js';
|
|
6
|
-
import { mapAgentMessages } from './message-mapper.js';
|
|
9
|
+
import { mapAgentMessages, extractMessageEntryIds, applyEntryIds } from './message-mapper.js';
|
|
10
|
+
import { getGitBranch } from './git-branch.js';
|
|
7
11
|
/** Parse data-URL encoded images into the shape the pi SDK expects. */
|
|
8
12
|
function parseDataUrlImages(images) {
|
|
9
13
|
if (!images || images.length === 0)
|
|
@@ -15,6 +19,66 @@ function parseDataUrlImages(images) {
|
|
|
15
19
|
return { type: 'image', data: match[2], mimeType: match[1] };
|
|
16
20
|
});
|
|
17
21
|
}
|
|
22
|
+
const TREE_PREVIEW_MAX_CHARS = 200;
|
|
23
|
+
function truncatePreview(value) {
|
|
24
|
+
if (value.length <= TREE_PREVIEW_MAX_CHARS)
|
|
25
|
+
return value;
|
|
26
|
+
return `${value.slice(0, TREE_PREVIEW_MAX_CHARS - 3)}...`;
|
|
27
|
+
}
|
|
28
|
+
function textFromEntryContent(content) {
|
|
29
|
+
if (typeof content === 'string')
|
|
30
|
+
return content;
|
|
31
|
+
if (!Array.isArray(content))
|
|
32
|
+
return '';
|
|
33
|
+
return content
|
|
34
|
+
.filter((block) => {
|
|
35
|
+
if (!block || typeof block !== 'object')
|
|
36
|
+
return false;
|
|
37
|
+
const candidate = block;
|
|
38
|
+
return candidate.type === 'text' && typeof candidate.text === 'string';
|
|
39
|
+
})
|
|
40
|
+
.map((block) => block.text)
|
|
41
|
+
.join('\n');
|
|
42
|
+
}
|
|
43
|
+
function previewForEntry(entry) {
|
|
44
|
+
if (entry.type === 'message') {
|
|
45
|
+
const messageContent = entry.message?.content;
|
|
46
|
+
const text = textFromEntryContent(messageContent);
|
|
47
|
+
return truncatePreview(text || entry.type);
|
|
48
|
+
}
|
|
49
|
+
if (entry.type === 'custom_message') {
|
|
50
|
+
const text = textFromEntryContent(entry.content);
|
|
51
|
+
return truncatePreview(text || entry.type);
|
|
52
|
+
}
|
|
53
|
+
if (entry.type === 'compaction' || entry.type === 'branch_summary') {
|
|
54
|
+
const summary = typeof entry.summary === 'string' ? entry.summary : '';
|
|
55
|
+
return truncatePreview(summary || entry.type);
|
|
56
|
+
}
|
|
57
|
+
return entry.type;
|
|
58
|
+
}
|
|
59
|
+
/** Map pi SDK tree nodes to the wire transfer shape used by pimote clients. */
|
|
60
|
+
export function mapTreeNodes(nodes) {
|
|
61
|
+
return nodes.map((node) => {
|
|
62
|
+
const timestamp = typeof node.entry.timestamp === 'string' ? node.entry.timestamp : new Date(0).toISOString();
|
|
63
|
+
return {
|
|
64
|
+
id: node.entry.id,
|
|
65
|
+
type: node.entry.type,
|
|
66
|
+
role: node.entry.type === 'message'
|
|
67
|
+
? typeof node.entry.message.role === 'string'
|
|
68
|
+
? node.entry.message.role
|
|
69
|
+
: undefined
|
|
70
|
+
: 'role' in node.entry && typeof node.entry.role === 'string'
|
|
71
|
+
? node.entry.role
|
|
72
|
+
: undefined,
|
|
73
|
+
customType: 'customType' in node.entry && typeof node.entry.customType === 'string' ? node.entry.customType : undefined,
|
|
74
|
+
preview: previewForEntry(node.entry),
|
|
75
|
+
timestamp,
|
|
76
|
+
label: node.label,
|
|
77
|
+
labelTimestamp: node.labelTimestamp,
|
|
78
|
+
children: mapTreeNodes(node.children),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
18
82
|
/**
|
|
19
83
|
* Create command context actions for extension commands.
|
|
20
84
|
* Captures the ManagedSlot (stable lifetime), not a transient handler.
|
|
@@ -62,20 +126,6 @@ function createCommandContextActions(slot) {
|
|
|
62
126
|
reload: () => slot.session.reload(),
|
|
63
127
|
};
|
|
64
128
|
}
|
|
65
|
-
/** Resolve the current git branch for a directory. Returns null if not a git repo. */
|
|
66
|
-
function getGitBranch(cwd) {
|
|
67
|
-
try {
|
|
68
|
-
return (execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
69
|
-
cwd,
|
|
70
|
-
encoding: 'utf-8',
|
|
71
|
-
timeout: 2000,
|
|
72
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
73
|
-
}).trim() || null);
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
129
|
export class WsHandler {
|
|
80
130
|
sessionManager;
|
|
81
131
|
folderIndex;
|
|
@@ -131,7 +181,49 @@ export class WsHandler {
|
|
|
131
181
|
folder.activeStatus = null;
|
|
132
182
|
}
|
|
133
183
|
}
|
|
134
|
-
this.sendResponse(id, true, { folders });
|
|
184
|
+
this.sendResponse(id, true, { folders, roots: this.folderIndex.roots });
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'create_project': {
|
|
188
|
+
const name = command.name;
|
|
189
|
+
const root = command.root;
|
|
190
|
+
// Validate name: non-empty, no path separators, not . or ..
|
|
191
|
+
if (!name || name.includes('/') || name.includes(sep) || name === '.' || name === '..') {
|
|
192
|
+
this.sendResponse(id, false, undefined, 'Invalid project name');
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
// Validate root is one of the configured roots
|
|
196
|
+
if (!this.folderIndex.roots.includes(root)) {
|
|
197
|
+
this.sendResponse(id, false, undefined, 'Root is not a configured project root');
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
const folderPath = join(root, name);
|
|
201
|
+
// Check if directory already exists
|
|
202
|
+
try {
|
|
203
|
+
await stat(folderPath);
|
|
204
|
+
this.sendResponse(id, false, undefined, 'Directory already exists');
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
const code = err instanceof Error ? err.code : undefined;
|
|
209
|
+
if (code !== 'ENOENT') {
|
|
210
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
+
this.sendResponse(id, false, undefined, `Cannot access directory: ${message}`);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
// ENOENT — directory doesn't exist yet, proceed with creation
|
|
215
|
+
}
|
|
216
|
+
// Create directory and git init
|
|
217
|
+
try {
|
|
218
|
+
await mkdir(folderPath, { recursive: true });
|
|
219
|
+
await promisify(execFile)('git', ['init'], { cwd: folderPath });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
this.sendResponse(id, false, undefined, `Failed to create project: ${message}`);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
this.sendResponse(id, true, { folderPath });
|
|
135
227
|
break;
|
|
136
228
|
}
|
|
137
229
|
case 'list_sessions': {
|
|
@@ -158,6 +250,7 @@ export class WsHandler {
|
|
|
158
250
|
archived: archivedLookup.get(s.path) === true,
|
|
159
251
|
isOwnedByMe: sl ? sl.connection?.connectedClientId === this.clientId : false,
|
|
160
252
|
liveStatus: sl ? sl.sessionState.status : null,
|
|
253
|
+
cwd: s.cwd !== command.folderPath ? s.cwd : undefined,
|
|
161
254
|
};
|
|
162
255
|
})
|
|
163
256
|
.filter((s) => command.includeArchived || !s.archived);
|
|
@@ -179,6 +272,7 @@ export class WsHandler {
|
|
|
179
272
|
archived: false,
|
|
180
273
|
isOwnedByMe: slot.connection?.connectedClientId === this.clientId,
|
|
181
274
|
liveStatus: slot.sessionState.status,
|
|
275
|
+
cwd: slot.folderPath !== command.folderPath ? slot.folderPath : undefined,
|
|
182
276
|
});
|
|
183
277
|
}
|
|
184
278
|
}
|
|
@@ -311,21 +405,23 @@ export class WsHandler {
|
|
|
311
405
|
break;
|
|
312
406
|
}
|
|
313
407
|
case 'archive_session': {
|
|
314
|
-
const
|
|
408
|
+
const archiveSessionIds = command.sessionIds;
|
|
315
409
|
const archiveFolderPath = command.folderPath;
|
|
316
|
-
if (!
|
|
317
|
-
this.sendResponse(id, false, undefined, '
|
|
410
|
+
if (!archiveSessionIds?.length || !archiveFolderPath) {
|
|
411
|
+
this.sendResponse(id, false, undefined, 'sessionIds and folderPath are required');
|
|
318
412
|
break;
|
|
319
413
|
}
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
414
|
+
let archivedCount = 0;
|
|
415
|
+
for (const archiveSessionId of archiveSessionIds) {
|
|
416
|
+
const archiveSlot = this.sessionManager.getSession(archiveSessionId);
|
|
417
|
+
const archiveSessionPath = archiveSlot?.session.sessionFile ?? (await this.folderIndex.resolveSessionPath(archiveFolderPath, archiveSessionId));
|
|
418
|
+
if (!archiveSessionPath)
|
|
419
|
+
continue;
|
|
420
|
+
await this.sessionMetadataStore.setArchived(archiveSessionPath, command.archived);
|
|
421
|
+
this.broadcastSessionArchived(archiveSessionId, archiveFolderPath, command.archived);
|
|
422
|
+
archivedCount++;
|
|
325
423
|
}
|
|
326
|
-
|
|
327
|
-
this.broadcastSessionArchived(archiveSessionId, archiveFolderPath, command.archived);
|
|
328
|
-
this.sendResponse(id, true, { archived: command.archived });
|
|
424
|
+
this.sendResponse(id, true, { archived: command.archived, count: archivedCount });
|
|
329
425
|
break;
|
|
330
426
|
}
|
|
331
427
|
case 'rename_session': {
|
|
@@ -495,7 +591,10 @@ export class WsHandler {
|
|
|
495
591
|
case 'get_commands':
|
|
496
592
|
case 'complete_args':
|
|
497
593
|
case 'set_session_name':
|
|
498
|
-
case 'dequeue_steering':
|
|
594
|
+
case 'dequeue_steering':
|
|
595
|
+
case 'fork':
|
|
596
|
+
case 'navigate_tree':
|
|
597
|
+
case 'set_tree_label': {
|
|
499
598
|
await this.handleSessionCommand(command, id);
|
|
500
599
|
break;
|
|
501
600
|
}
|
|
@@ -538,6 +637,12 @@ export class WsHandler {
|
|
|
538
637
|
this.sendResponse(id, true);
|
|
539
638
|
break;
|
|
540
639
|
}
|
|
640
|
+
if (trimmed === '/tree') {
|
|
641
|
+
const tree = mapTreeNodes(session.sessionManager.getTree());
|
|
642
|
+
const currentLeafId = session.sessionManager.getLeafId();
|
|
643
|
+
this.sendResponse(id, true, { tree, currentLeafId });
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
541
646
|
session.prompt(command.message, { images: parseDataUrlImages(command.images) }).catch((err) => {
|
|
542
647
|
console.error(`[WsHandler] prompt error:`, err);
|
|
543
648
|
});
|
|
@@ -566,7 +671,10 @@ export class WsHandler {
|
|
|
566
671
|
case 'abort': {
|
|
567
672
|
// Resolve pending UI responses first so stuck dialogs unblock
|
|
568
673
|
resolveAllSlotPendingUi(slot);
|
|
569
|
-
await session.abort();
|
|
674
|
+
const abortResult = await Promise.race([session.abort().then(() => 'ok'), new Promise((resolve) => setTimeout(() => resolve('timeout'), 30_000))]);
|
|
675
|
+
if (abortResult === 'timeout') {
|
|
676
|
+
console.error(`[WsHandler] session.abort() did not resolve within 30s (sessionId=${sessionId})`);
|
|
677
|
+
}
|
|
570
678
|
this.sendResponse(id, true);
|
|
571
679
|
break;
|
|
572
680
|
}
|
|
@@ -630,6 +738,7 @@ export class WsHandler {
|
|
|
630
738
|
const state = {
|
|
631
739
|
model: model ? { provider: model.provider, id: model.id, name: model.name } : null,
|
|
632
740
|
thinkingLevel: session.thinkingLevel,
|
|
741
|
+
availableThinkingLevels: session.getAvailableThinkingLevels(),
|
|
633
742
|
isStreaming: session.isStreaming,
|
|
634
743
|
isCompacting: session.isCompacting,
|
|
635
744
|
sessionFile: session.sessionFile,
|
|
@@ -643,6 +752,8 @@ export class WsHandler {
|
|
|
643
752
|
}
|
|
644
753
|
case 'get_messages': {
|
|
645
754
|
const messages = mapAgentMessages(session.messages);
|
|
755
|
+
const entryIds = extractMessageEntryIds(session.sessionManager.getBranch());
|
|
756
|
+
applyEntryIds(messages, entryIds);
|
|
646
757
|
this.sendResponse(id, true, { messages });
|
|
647
758
|
break;
|
|
648
759
|
}
|
|
@@ -696,7 +807,7 @@ export class WsHandler {
|
|
|
696
807
|
});
|
|
697
808
|
}
|
|
698
809
|
// Pimote built-in commands
|
|
699
|
-
commands.push({ name: 'new', description: 'Start a new session', hasArgCompletions: false }, { name: 'reload', description: 'Reload extensions and skills', hasArgCompletions: false });
|
|
810
|
+
commands.push({ name: 'new', description: 'Start a new session', hasArgCompletions: false }, { name: 'reload', description: 'Reload extensions and skills', hasArgCompletions: false }, { name: 'tree', description: 'Navigate session history tree', hasArgCompletions: false });
|
|
700
811
|
this.sendResponse(id, true, { commands });
|
|
701
812
|
break;
|
|
702
813
|
}
|
|
@@ -720,6 +831,68 @@ export class WsHandler {
|
|
|
720
831
|
this.sendResponse(id, true);
|
|
721
832
|
break;
|
|
722
833
|
}
|
|
834
|
+
case 'fork': {
|
|
835
|
+
if (!command.entryId) {
|
|
836
|
+
this.sendResponse(id, false, undefined, 'entryId is required');
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
const forkResult = await slot.runtime.fork(command.entryId);
|
|
840
|
+
if (!forkResult.cancelled) {
|
|
841
|
+
await this.handleSessionReset(slot);
|
|
842
|
+
}
|
|
843
|
+
const forkData = { cancelled: forkResult.cancelled };
|
|
844
|
+
if (forkResult.selectedText !== undefined) {
|
|
845
|
+
forkData.selectedText = forkResult.selectedText;
|
|
846
|
+
}
|
|
847
|
+
this.sendResponse(id, true, forkData);
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case 'navigate_tree': {
|
|
851
|
+
if (slot.sessionState.treeNavigationInProgress) {
|
|
852
|
+
this.sendResponse(id, false, undefined, 'Tree navigation already in progress');
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
const options = {};
|
|
856
|
+
if (command.summarize !== undefined)
|
|
857
|
+
options.summarize = command.summarize;
|
|
858
|
+
if (command.customInstructions !== undefined)
|
|
859
|
+
options.customInstructions = command.customInstructions;
|
|
860
|
+
if (command.replaceInstructions !== undefined)
|
|
861
|
+
options.replaceInstructions = command.replaceInstructions;
|
|
862
|
+
if (command.label !== undefined)
|
|
863
|
+
options.label = command.label;
|
|
864
|
+
slot.sessionState.treeNavigationInProgress = true;
|
|
865
|
+
this.emitBufferedSessionEvent(slot, sessionId, {
|
|
866
|
+
type: 'tree_navigation_start',
|
|
867
|
+
targetId: command.targetId,
|
|
868
|
+
summarizing: !!command.summarize,
|
|
869
|
+
});
|
|
870
|
+
let result;
|
|
871
|
+
try {
|
|
872
|
+
result = (await session.navigateTree(command.targetId, options));
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
slot.sessionState.treeNavigationInProgress = false;
|
|
876
|
+
this.emitBufferedSessionEvent(slot, sessionId, {
|
|
877
|
+
type: 'tree_navigation_end',
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (!result.cancelled) {
|
|
881
|
+
await this.handleSessionReset(slot);
|
|
882
|
+
}
|
|
883
|
+
const data = { cancelled: result.cancelled };
|
|
884
|
+
if (result.editorText !== undefined) {
|
|
885
|
+
data.editorText = result.editorText;
|
|
886
|
+
}
|
|
887
|
+
this.sendResponse(id, true, data);
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
case 'set_tree_label': {
|
|
891
|
+
const normalizedLabel = command.label === '' ? undefined : command.label;
|
|
892
|
+
session.sessionManager.appendLabelChange(command.entryId, normalizedLabel);
|
|
893
|
+
this.sendResponse(id, true, { success: true });
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
723
896
|
}
|
|
724
897
|
}
|
|
725
898
|
/** Notify the old owner that they've been displaced from a session.
|
|
@@ -935,6 +1108,30 @@ export class WsHandler {
|
|
|
935
1108
|
console.error('[WsHandler] Failed to send event:', err);
|
|
936
1109
|
}
|
|
937
1110
|
}
|
|
1111
|
+
emitBufferedSessionEvent(slot, sessionId, sdkEvent) {
|
|
1112
|
+
let forwarded = false;
|
|
1113
|
+
slot.sessionState.eventBuffer.onEvent(sdkEvent, sessionId, (event) => {
|
|
1114
|
+
// Augment agent_end with message entry IDs so the client can enable
|
|
1115
|
+
// fork targets on messages that arrived via streaming (without IDs).
|
|
1116
|
+
if (event.type === 'agent_end') {
|
|
1117
|
+
const entryIds = extractMessageEntryIds(slot.session.sessionManager.getBranch());
|
|
1118
|
+
event.messageEntryIds = entryIds;
|
|
1119
|
+
}
|
|
1120
|
+
forwarded = true;
|
|
1121
|
+
this.sendEvent(event);
|
|
1122
|
+
}, () => slot.session.messages[slot.session.messages.length - 1]);
|
|
1123
|
+
// Test doubles for EventBuffer may no-op on onEvent().
|
|
1124
|
+
// Fallback keeps lifecycle visibility in tests while real runtime uses buffered forwarding above.
|
|
1125
|
+
if (!forwarded) {
|
|
1126
|
+
const cursorBase = slot.sessionState.eventBuffer.currentCursor;
|
|
1127
|
+
const cursor = typeof cursorBase === 'number' ? cursorBase + 1 : 1;
|
|
1128
|
+
this.sendEvent({
|
|
1129
|
+
...sdkEvent,
|
|
1130
|
+
sessionId,
|
|
1131
|
+
cursor,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
938
1135
|
/** Send a full_resync event to the client for the given managed session.
|
|
939
1136
|
* Used when the underlying pi session is reset (newSession, switchSession, fork, navigateTree). */
|
|
940
1137
|
sendFullResyncForSession(pimoteSessionId, slot) {
|
|
@@ -943,6 +1140,7 @@ export class WsHandler {
|
|
|
943
1140
|
const state = {
|
|
944
1141
|
model: model ? { provider: model.provider, id: model.id, name: model.name } : null,
|
|
945
1142
|
thinkingLevel: session.thinkingLevel,
|
|
1143
|
+
availableThinkingLevels: session.getAvailableThinkingLevels(),
|
|
946
1144
|
isStreaming: session.isStreaming,
|
|
947
1145
|
isCompacting: session.isCompacting,
|
|
948
1146
|
sessionFile: session.sessionFile,
|
|
@@ -952,6 +1150,8 @@ export class WsHandler {
|
|
|
952
1150
|
messageCount: session.messages.length,
|
|
953
1151
|
};
|
|
954
1152
|
const messages = mapAgentMessages(session.messages);
|
|
1153
|
+
const entryIds = extractMessageEntryIds(session.sessionManager.getBranch());
|
|
1154
|
+
applyEntryIds(messages, entryIds);
|
|
955
1155
|
const fullResyncEvent = {
|
|
956
1156
|
type: 'full_resync',
|
|
957
1157
|
sessionId: pimoteSessionId,
|
|
@@ -1007,6 +1207,7 @@ export class WsHandler {
|
|
|
1007
1207
|
sessionName,
|
|
1008
1208
|
firstMessage,
|
|
1009
1209
|
messageCount,
|
|
1210
|
+
gitBranch: slot ? getGitBranch(slot.folderPath) : null,
|
|
1010
1211
|
};
|
|
1011
1212
|
for (const [, handler] of clientRegistry) {
|
|
1012
1213
|
handler.sendToClient(event);
|