pi-ui-extend 0.1.35 → 0.1.37
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/dist/app/app.d.ts +8 -0
- package/dist/app/app.js +48 -5
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +1 -0
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +32 -0
- package/dist/app/commands/command-navigation-actions.js +3 -0
- package/dist/app/commands/command-registry.d.ts +1 -0
- package/dist/app/commands/command-registry.js +8 -0
- package/dist/app/commands/command-session-actions.d.ts +2 -0
- package/dist/app/commands/command-session-actions.js +81 -1
- package/dist/app/extensions/extension-actions-controller.d.ts +5 -1
- package/dist/app/extensions/extension-actions-controller.js +35 -2
- package/dist/app/input/input-controller.d.ts +2 -0
- package/dist/app/input/input-controller.js +50 -2
- package/dist/app/input/terminal-edit-shortcuts.d.ts +2 -0
- package/dist/app/input/terminal-edit-shortcuts.js +49 -0
- package/dist/app/input/voice-controller.js +1 -1
- package/dist/app/popup/popup-action-controller.d.ts +2 -3
- package/dist/app/popup/popup-action-controller.js +2 -5
- package/dist/app/rendering/message-content.js +4 -3
- package/dist/app/rendering/render-controller.js +21 -38
- package/dist/app/rendering/status-line-renderer.d.ts +1 -0
- package/dist/app/rendering/status-line-renderer.js +14 -2
- package/dist/app/runtime.js +12 -2
- package/dist/app/screen/mouse-controller.js +2 -0
- package/dist/app/session/session-event-controller.d.ts +7 -0
- package/dist/app/session/session-event-controller.js +10 -13
- package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
- package/dist/app/session/session-lifecycle-controller.js +7 -0
- package/dist/app/session/tabs-controller.d.ts +1 -0
- package/dist/app/session/tabs-controller.js +1 -0
- package/dist/app/terminal/terminal-controller.js +1 -0
- package/dist/app/terminal/terminal-output-buffer.d.ts +8 -6
- package/dist/app/terminal/terminal-output-buffer.js +24 -16
- package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
- package/dist/app/workspace/workspace-actions-controller.js +1 -0
- package/dist/bundled-extensions/terminal-bell/index.js +118 -33
- package/dist/markdown-format.d.ts +1 -0
- package/dist/markdown-format.js +30 -16
- package/dist/schemas/pi-tools-suite-schema.d.ts +5 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/tool-renderers/apply-patch.js +6 -1
- package/dist/tool-renderers/patch-normalize.d.ts +24 -0
- package/dist/tool-renderers/patch-normalize.js +163 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/package.json +5 -5
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +15 -2
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +36 -19
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +5 -2
- package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +8 -3
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +63 -28
- package/external/pi-tools-suite/src/async-subagents/core/tool-guard.ts +9 -4
- package/external/pi-tools-suite/src/comment-checker/config.ts +98 -0
- package/external/pi-tools-suite/src/comment-checker/detect.ts +215 -0
- package/external/pi-tools-suite/src/comment-checker/index.ts +294 -0
- package/external/pi-tools-suite/src/dcp/commands.ts +29 -15
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +111 -60
- package/external/pi-tools-suite/src/dcp/config.ts +10 -6
- package/external/pi-tools-suite/src/dcp/debug-log.ts +235 -0
- package/external/pi-tools-suite/src/dcp/index.ts +204 -27
- package/external/pi-tools-suite/src/dcp/prompts.ts +25 -28
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +19 -1
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +36 -58
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +18 -0
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +4 -2
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +31 -2
- package/external/pi-tools-suite/src/dcp/state.ts +62 -4
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +18 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +11 -3
- package/external/pi-tools-suite/src/telegram-mirror/index.ts +1 -1
- package/external/pi-tools-suite/src/todo/index.ts +24 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +3 -3
- package/external/pi-tools-suite/src/usage/index.ts +18 -4
- package/package.json +4 -4
- package/schemas/pi-tools-suite.json +24 -0
package/dist/app/app.d.ts
CHANGED
|
@@ -43,6 +43,13 @@ export declare class PiUiExtendApp {
|
|
|
43
43
|
private readonly toastNotifier;
|
|
44
44
|
private readonly extensionShutdownHandler;
|
|
45
45
|
private runtime;
|
|
46
|
+
/**
|
|
47
|
+
* Maps each session runtime to the isolated extension event bus it was
|
|
48
|
+
* created with. The renderer uses this to emit signals (e.g. retry state)
|
|
49
|
+
* back to the extensions running inside a specific runtime/tab, so a signal
|
|
50
|
+
* for one tab never reaches another tab's extensions.
|
|
51
|
+
*/
|
|
52
|
+
private readonly extensionEventBusByRuntime;
|
|
46
53
|
private readonly inputEditor;
|
|
47
54
|
private lastInputEditorContentVersion;
|
|
48
55
|
private readonly requestHistory;
|
|
@@ -66,6 +73,7 @@ export declare class PiUiExtendApp {
|
|
|
66
73
|
start(): Promise<void>;
|
|
67
74
|
private checkPixUpdateOnStartup;
|
|
68
75
|
private bindCurrentSession;
|
|
76
|
+
private awaitCurrentSessionExtensions;
|
|
69
77
|
private activateRuntime;
|
|
70
78
|
private createExtensionEventBus;
|
|
71
79
|
private handleTerminalBellAttention;
|
package/dist/app/app.js
CHANGED
|
@@ -112,6 +112,13 @@ export class PiUiExtendApp {
|
|
|
112
112
|
};
|
|
113
113
|
extensionShutdownHandler = () => { };
|
|
114
114
|
runtime;
|
|
115
|
+
/**
|
|
116
|
+
* Maps each session runtime to the isolated extension event bus it was
|
|
117
|
+
* created with. The renderer uses this to emit signals (e.g. retry state)
|
|
118
|
+
* back to the extensions running inside a specific runtime/tab, so a signal
|
|
119
|
+
* for one tab never reaches another tab's extensions.
|
|
120
|
+
*/
|
|
121
|
+
extensionEventBusByRuntime = new WeakMap();
|
|
115
122
|
inputEditor = new InputEditor();
|
|
116
123
|
lastInputEditorContentVersion = this.inputEditor.contentVersion;
|
|
117
124
|
requestHistory;
|
|
@@ -166,12 +173,24 @@ export class PiUiExtendApp {
|
|
|
166
173
|
maxProjectSessions: () => this.pixConfig.maxProjectSessions,
|
|
167
174
|
blinkController: this.blinkController,
|
|
168
175
|
runtime: () => this.runtime,
|
|
169
|
-
createRuntimeForNewSession: () => this.createRuntime(
|
|
176
|
+
createRuntimeForNewSession: () => this.createRuntime(
|
|
177
|
+
// Never reuse services across tabs. The SDK ties the extension
|
|
178
|
+
// runtime (resourceLoader.getExtensions().runtime) to the
|
|
179
|
+
// resourceLoader, and that runtime is shared by every session
|
|
180
|
+
// built from the same services. When any such session is
|
|
181
|
+
// disposed (tab close / session replacement / reload), the SDK
|
|
182
|
+
// invalidates that shared runtime, which makes every sibling
|
|
183
|
+
// session's captured `pi`/ctx stale — session_start handlers
|
|
184
|
+
// then throw "ctx is stale after session replacement or reload".
|
|
185
|
+
// Fresh services per session = fresh extension runtime = no
|
|
186
|
+
// cross-session invalidation.
|
|
187
|
+
newTabRuntimeOptions(this.options)),
|
|
170
188
|
createRuntimeForSession: (sessionPath) => this.createRuntime({
|
|
171
189
|
...this.options,
|
|
172
190
|
noSession: false,
|
|
173
191
|
sessionPath,
|
|
174
192
|
}),
|
|
193
|
+
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
175
194
|
activateRuntime: (runtime, options) => this.activateRuntime(runtime, options),
|
|
176
195
|
disposeRuntime: (runtime) => this.terminalController.disposeRuntime(runtime),
|
|
177
196
|
isRunning: () => this.running,
|
|
@@ -317,6 +336,7 @@ export class PiUiExtendApp {
|
|
|
317
336
|
isRunning: () => this.running,
|
|
318
337
|
getInput: () => this.input,
|
|
319
338
|
setInput: (value) => this.setInput(value),
|
|
339
|
+
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
320
340
|
resetSessionView: () => this.resetSessionView(),
|
|
321
341
|
loadSessionHistory: () => this.loadSessionHistory(),
|
|
322
342
|
afterSessionReplacement: (message) => this.afterSessionReplacement(message),
|
|
@@ -341,6 +361,7 @@ export class PiUiExtendApp {
|
|
|
341
361
|
this.workspaceActions = new AppWorkspaceActionsController({
|
|
342
362
|
entries: this.entries,
|
|
343
363
|
runtime: () => this.runtime,
|
|
364
|
+
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
344
365
|
findUserEntry: (entryId) => this.findUserEntry(entryId),
|
|
345
366
|
touchEntry: (entry) => this.touchEntry(entry),
|
|
346
367
|
resetSessionView: () => this.resetSessionView(),
|
|
@@ -370,6 +391,16 @@ export class PiUiExtendApp {
|
|
|
370
391
|
setSessionActivity: (activity) => this.setSessionActivity(activity),
|
|
371
392
|
updateQueuedMessageStatus: () => this.queuedMessages.updateQueuedMessageStatus(),
|
|
372
393
|
flushAutoUserMessages: () => { void this.queuedMessages.flushAutoUserMessages(); },
|
|
394
|
+
emitExtensionEvent: (channel, data) => {
|
|
395
|
+
// Emit a signal to the extensions running inside the active
|
|
396
|
+
// runtime's event bus. Used to inform extensions (terminal-bell,
|
|
397
|
+
// todo) about session lifecycle state the SDK doesn't forward to
|
|
398
|
+
// them, such as auto-retry being in progress.
|
|
399
|
+
const runtime = this.runtime;
|
|
400
|
+
if (!runtime)
|
|
401
|
+
return;
|
|
402
|
+
this.extensionEventBusByRuntime.get(runtime)?.emit(channel, data);
|
|
403
|
+
},
|
|
373
404
|
prepareWorkspaceMutation: (toolName, args) => this.workspaceActions.prepareWorkspaceMutation(toolName, args),
|
|
374
405
|
workspaceMutationFromToolExecution: (input) => this.workspaceActions.workspaceMutationFromToolExecution(input),
|
|
375
406
|
recordWorkspaceMutationForUserEntry: (entryId, mutation) => this.workspaceActions.recordWorkspaceMutationForUserEntry(entryId, mutation),
|
|
@@ -471,6 +502,7 @@ export class PiUiExtendApp {
|
|
|
471
502
|
showMenu: (items, options) => this.popupMenus.menuController.show(items, options),
|
|
472
503
|
getModelMenuItems: (query) => this.menuItems.getModelMenuItems(query),
|
|
473
504
|
getThinkingMenuItems: (query) => this.menuItems.getThinkingMenuItems(query),
|
|
505
|
+
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
474
506
|
modelRef: (model) => this.menuItems.modelRef(model),
|
|
475
507
|
getFavoriteScopedModels: () => this.menuItems.getFavoriteScopedModels(),
|
|
476
508
|
setSessionStatus: (session) => this.setSessionStatus(session),
|
|
@@ -518,9 +550,8 @@ export class PiUiExtendApp {
|
|
|
518
550
|
setSessionStatus: (session) => this.setSessionStatus(session),
|
|
519
551
|
showToast: (message, kind) => this.showToast(message, kind),
|
|
520
552
|
render: () => this.render(),
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
loadSessionHistory: () => this.loadSessionHistory(),
|
|
553
|
+
awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
|
|
554
|
+
afterSessionReplacement: (message) => this.afterSessionReplacement(message),
|
|
524
555
|
scrollToConversationEntry: (entryId) => this.scrollController.scrollToConversationEntry(entryId),
|
|
525
556
|
scrollToUserMessageJumpTarget: (target) => this.scrollToUserMessageJumpTarget(target),
|
|
526
557
|
}, this.popupMenus, this.commandController, this.menuItems, this.queuedMessages, this.workspaceActions);
|
|
@@ -763,10 +794,16 @@ export class PiUiExtendApp {
|
|
|
763
794
|
this.slashCommands = this.commandController.slashCommands;
|
|
764
795
|
}
|
|
765
796
|
createRuntime(options, runtimeOptions = {}) {
|
|
797
|
+
const eventBus = this.createExtensionEventBus();
|
|
766
798
|
return createPixRuntime(options, {
|
|
767
|
-
eventBus
|
|
799
|
+
eventBus,
|
|
768
800
|
config: this.pixConfig,
|
|
769
801
|
...runtimeOptions,
|
|
802
|
+
}).then((runtime) => {
|
|
803
|
+
// Record the bus this runtime's extensions use, so the renderer can
|
|
804
|
+
// emit signals (retry state, etc.) targeted at this runtime's extensions.
|
|
805
|
+
this.extensionEventBusByRuntime.set(runtime, eventBus);
|
|
806
|
+
return runtime;
|
|
770
807
|
});
|
|
771
808
|
}
|
|
772
809
|
async loadStartupConfig() {
|
|
@@ -824,11 +861,17 @@ export class PiUiExtendApp {
|
|
|
824
861
|
async bindCurrentSession(options) {
|
|
825
862
|
await this.sessionLifecycle.bindCurrentSession(options);
|
|
826
863
|
}
|
|
864
|
+
async awaitCurrentSessionExtensions(runtime) {
|
|
865
|
+
await this.sessionLifecycle.awaitCurrentSessionExtensions(runtime);
|
|
866
|
+
}
|
|
827
867
|
async activateRuntime(runtime, options) {
|
|
828
868
|
this.runtime = runtime;
|
|
829
869
|
runtime.setRebindSession(async () => {
|
|
830
870
|
await this.bindCurrentSession({ awaitExtensions: false });
|
|
831
871
|
});
|
|
872
|
+
runtime.setBeforeSessionInvalidate(() => {
|
|
873
|
+
this.extensionUiController.clearWidgets(this.activeExtensionUiScope());
|
|
874
|
+
});
|
|
832
875
|
await this.bindCurrentSession(options);
|
|
833
876
|
}
|
|
834
877
|
createExtensionEventBus() {
|
|
@@ -49,6 +49,7 @@ export class AppCommandController {
|
|
|
49
49
|
runReloadCommand: () => this.sessionActions.runReloadCommand(),
|
|
50
50
|
runNewSessionCommand: () => this.sessionActions.runNewSessionCommand(),
|
|
51
51
|
runNewTabCommand: () => this.host.openNewTab(),
|
|
52
|
+
runDeleteCommand: (argumentsText) => this.sessionActions.runDeleteCommand(argumentsText),
|
|
52
53
|
runCompactCommand: (customInstructions) => this.sessionActions.runCompactCommand(customInstructions),
|
|
53
54
|
runForkCommand: (argumentsText) => this.navigationActions.runForkCommand(argumentsText),
|
|
54
55
|
runCloneCommand: () => this.navigationActions.runCloneCommand(),
|
|
@@ -7,6 +7,7 @@ export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
|
|
|
7
7
|
export type CommandControllerHost = {
|
|
8
8
|
readonly options: AppOptions;
|
|
9
9
|
runtime(): AgentSessionRuntime | undefined;
|
|
10
|
+
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
|
|
10
11
|
requestHistory(): AppRequestHistory;
|
|
11
12
|
getInput(): string;
|
|
12
13
|
setInput(value: string): void;
|
|
@@ -14,6 +14,7 @@ export declare class ModelCommandActions {
|
|
|
14
14
|
runModelCommand(model: SessionModel): Promise<void>;
|
|
15
15
|
runThinkingCommand(level: ThinkingLevel): Promise<void>;
|
|
16
16
|
private addPersistentSystemEntry;
|
|
17
|
+
private reloadAfterModelChange;
|
|
17
18
|
private saveDefaultModel;
|
|
18
19
|
private saveDefaultThinking;
|
|
19
20
|
}
|
|
@@ -269,6 +269,17 @@ export class ModelCommandActions {
|
|
|
269
269
|
this.host.render();
|
|
270
270
|
await runtime.session.setModel(model);
|
|
271
271
|
this.host.addEntry({ id: createId("system"), kind: "system", text: `Selected model ${ref}` });
|
|
272
|
+
if (runtime.session.isStreaming) {
|
|
273
|
+
this.host.addEntry({
|
|
274
|
+
id: createId("system"),
|
|
275
|
+
kind: "system",
|
|
276
|
+
text: "Skipped reload because the agent is still running. Run /reload when idle to refresh model-specific tools.",
|
|
277
|
+
});
|
|
278
|
+
this.host.toast.warning("Model changed; reload skipped while the agent is running");
|
|
279
|
+
this.host.setSessionStatus(runtime.session);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await this.reloadAfterModelChange(runtime.session, ref);
|
|
272
283
|
this.host.setSessionStatus(runtime.session);
|
|
273
284
|
}
|
|
274
285
|
async runThinkingCommand(level) {
|
|
@@ -285,6 +296,27 @@ export class ModelCommandActions {
|
|
|
285
296
|
appendPixSystemDisplayEntry(session, text);
|
|
286
297
|
this.host.addEntry({ id: createId("system"), kind: "system", text });
|
|
287
298
|
}
|
|
299
|
+
async reloadAfterModelChange(session, ref) {
|
|
300
|
+
this.host.setStatus(`reloading resources for ${ref}`);
|
|
301
|
+
this.host.render();
|
|
302
|
+
try {
|
|
303
|
+
await session.reload();
|
|
304
|
+
this.host.addEntry({
|
|
305
|
+
id: createId("system"),
|
|
306
|
+
kind: "system",
|
|
307
|
+
text: `Reloaded resources after model change to ${ref}`,
|
|
308
|
+
});
|
|
309
|
+
this.host.toast.success("Model changed and resources reloaded");
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
this.host.addEntry({
|
|
313
|
+
id: createId("error"),
|
|
314
|
+
kind: "error",
|
|
315
|
+
text: `Model changed to ${ref}, but reload failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
316
|
+
});
|
|
317
|
+
this.host.toast.error("Model changed, but reload failed");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
288
320
|
saveDefaultModel(modelRef) {
|
|
289
321
|
const saved = savePixDefaultModel(modelRef);
|
|
290
322
|
if (!saved)
|
|
@@ -50,6 +50,7 @@ export class NavigationCommandActions {
|
|
|
50
50
|
throw new Error("No user messages to fork from");
|
|
51
51
|
this.host.setStatus("forking session");
|
|
52
52
|
this.host.render();
|
|
53
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
53
54
|
const result = await runtime.fork(entryId);
|
|
54
55
|
if (result.cancelled) {
|
|
55
56
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
|
|
@@ -74,6 +75,7 @@ export class NavigationCommandActions {
|
|
|
74
75
|
}
|
|
75
76
|
this.host.setStatus("cloning session");
|
|
76
77
|
this.host.render();
|
|
78
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
77
79
|
const result = await runtime.fork(leafId, { position: "at" });
|
|
78
80
|
if (result.cancelled) {
|
|
79
81
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Clone cancelled." });
|
|
@@ -228,6 +230,7 @@ export class NavigationCommandActions {
|
|
|
228
230
|
const resolvedSessionPath = resolve(runtime.cwd, sessionPath);
|
|
229
231
|
this.host.setStatus("switching session");
|
|
230
232
|
this.host.render();
|
|
233
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
231
234
|
const result = await runtime.switchSession(resolvedSessionPath);
|
|
232
235
|
if (result.cancelled) {
|
|
233
236
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Resume cancelled." });
|
|
@@ -33,6 +33,7 @@ export type CommandRegistryActions = {
|
|
|
33
33
|
runResumeCommand(): Promise<void>;
|
|
34
34
|
runNewSessionCommand(): Promise<void>;
|
|
35
35
|
runNewTabCommand(): Promise<void>;
|
|
36
|
+
runDeleteCommand(argumentsText: string): Promise<void>;
|
|
36
37
|
runCompactCommand(customInstructions?: string): Promise<void>;
|
|
37
38
|
};
|
|
38
39
|
export declare function createSlashCommands(actions: CommandRegistryActions, host: CommandControllerHost): readonly SlashCommand[];
|
|
@@ -247,6 +247,14 @@ export function createSlashCommands(actions, host) {
|
|
|
247
247
|
keywords: ["tab", "session", "fresh", "new"],
|
|
248
248
|
run: () => actions.runNewTabCommand(),
|
|
249
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: "delete",
|
|
252
|
+
description: "Delete the current (or specified) session file plus its sidecar DCP state",
|
|
253
|
+
kind: "builtin",
|
|
254
|
+
keywords: ["remove", "destroy", "purge", "session", "sidecar", "dcp"],
|
|
255
|
+
allowArguments: true,
|
|
256
|
+
run: (argumentsText) => actions.runDeleteCommand(argumentsText),
|
|
257
|
+
},
|
|
250
258
|
{
|
|
251
259
|
name: "compact",
|
|
252
260
|
description: "Manually compact the session context",
|
|
@@ -14,6 +14,8 @@ export declare class SessionCommandActions {
|
|
|
14
14
|
runUpdateCommand(argumentsText: string): Promise<void>;
|
|
15
15
|
runHotkeysCommand(): Promise<void>;
|
|
16
16
|
runReloadCommand(): Promise<void>;
|
|
17
|
+
runDeleteCommand(argumentsText: string): Promise<void>;
|
|
18
|
+
private removeDcpSidecarState;
|
|
17
19
|
runNewSessionCommand(): Promise<void>;
|
|
18
20
|
runCompactCommand(customInstructions?: string): Promise<void>;
|
|
19
21
|
private piPackageRoot;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { getIdleRuntime, getRuntime, parsePathArgument } from "./command-runtime.js";
|
|
@@ -260,6 +260,7 @@ export class SessionCommandActions {
|
|
|
260
260
|
this.host.setStatus("reloading");
|
|
261
261
|
this.host.render();
|
|
262
262
|
try {
|
|
263
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
263
264
|
await runtime.session.reload();
|
|
264
265
|
this.host.setSessionStatus(runtime.session);
|
|
265
266
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "Reloaded keybindings, extensions, skills, prompts, themes" });
|
|
@@ -271,12 +272,86 @@ export class SessionCommandActions {
|
|
|
271
272
|
this.host.toast.error("Reload failed");
|
|
272
273
|
}
|
|
273
274
|
}
|
|
275
|
+
async runDeleteCommand(argumentsText) {
|
|
276
|
+
const runtime = getIdleRuntime(this.host, "delete");
|
|
277
|
+
if (!runtime)
|
|
278
|
+
return;
|
|
279
|
+
const sessionManager = runtime.session.sessionManager;
|
|
280
|
+
const currentSessionFile = sessionManager.getSessionFile();
|
|
281
|
+
const currentSessionId = sessionManager.getSessionId();
|
|
282
|
+
const targetArgument = argumentsText.trim();
|
|
283
|
+
const isCurrent = !targetArgument;
|
|
284
|
+
const targetSessionFile = targetArgument ? resolve(runtime.cwd, targetArgument) : currentSessionFile;
|
|
285
|
+
if (!targetSessionFile) {
|
|
286
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: "Nothing to delete: this session is in-memory and not persisted." });
|
|
287
|
+
this.host.toast.info("Nothing to delete");
|
|
288
|
+
this.host.setSessionStatus(runtime.session);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const targetSessionId = parseSessionIdFromFileName(targetSessionFile) ?? currentSessionId;
|
|
292
|
+
const confirm = await this.host.showMenu([
|
|
293
|
+
{
|
|
294
|
+
value: true,
|
|
295
|
+
label: `Yes, delete ${isCurrent ? "the current session" : basename(targetSessionFile)}`,
|
|
296
|
+
description: "This permanently removes the session file and any sidecar data. This cannot be undone.",
|
|
297
|
+
variant: "error",
|
|
298
|
+
},
|
|
299
|
+
{ value: false, label: "Cancel" },
|
|
300
|
+
], { title: "Delete session?", searchable: false, preserveStatus: true });
|
|
301
|
+
if (confirm !== true) {
|
|
302
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete cancelled." });
|
|
303
|
+
this.host.setSessionStatus(runtime.session);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.host.setStatus("deleting session");
|
|
307
|
+
this.host.render();
|
|
308
|
+
const sidecarRemoved = await this.removeDcpSidecarState(dirname(targetSessionFile), targetSessionId).catch(() => false);
|
|
309
|
+
await rm(targetSessionFile, { force: true }).catch(() => undefined);
|
|
310
|
+
const deleteCurrent = isCurrent || targetSessionFile === currentSessionFile;
|
|
311
|
+
if (deleteCurrent) {
|
|
312
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
313
|
+
const result = await runtime.newSession();
|
|
314
|
+
if (result.cancelled) {
|
|
315
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: "Delete succeeded, but new session was cancelled." });
|
|
316
|
+
this.host.setSessionStatus(runtime.session);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.host.resetSessionView();
|
|
320
|
+
this.host.loadSessionHistory();
|
|
321
|
+
this.host.addEntry({
|
|
322
|
+
id: createId("system"),
|
|
323
|
+
kind: "system",
|
|
324
|
+
text: `Deleted session ${targetSessionId}. ${sidecarRemoved ? "Sidecar DCP state removed. " : ""}Started a new session. cwd=${runtime.cwd}`,
|
|
325
|
+
});
|
|
326
|
+
if (runtime.modelFallbackMessage)
|
|
327
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
|
|
328
|
+
this.host.setSessionStatus(runtime.session);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
this.host.addEntry({
|
|
332
|
+
id: createId("system"),
|
|
333
|
+
kind: "system",
|
|
334
|
+
text: `Deleted session file ${targetSessionFile}${sidecarRemoved ? " and its sidecar DCP state" : ""}.`,
|
|
335
|
+
});
|
|
336
|
+
this.host.setSessionStatus(runtime.session);
|
|
337
|
+
}
|
|
338
|
+
this.host.toast.success("Session deleted");
|
|
339
|
+
}
|
|
340
|
+
async removeDcpSidecarState(sessionDir, sessionId) {
|
|
341
|
+
if (!sessionId)
|
|
342
|
+
return false;
|
|
343
|
+
const safeName = `${sessionId.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`;
|
|
344
|
+
const statePath = join(sessionDir, "dcp-state", safeName);
|
|
345
|
+
await rm(statePath, { force: true });
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
274
348
|
async runNewSessionCommand() {
|
|
275
349
|
const runtime = getIdleRuntime(this.host, "new");
|
|
276
350
|
if (!runtime)
|
|
277
351
|
return;
|
|
278
352
|
this.host.setStatus("starting new session");
|
|
279
353
|
this.host.render();
|
|
354
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
280
355
|
const result = await runtime.newSession();
|
|
281
356
|
if (result.cancelled) {
|
|
282
357
|
this.host.addEntry({ id: createId("system"), kind: "system", text: "New session cancelled." });
|
|
@@ -334,3 +409,8 @@ function splitUpdateArguments(argumentsText) {
|
|
|
334
409
|
const trimmed = argumentsText.trim();
|
|
335
410
|
return trimmed ? trimmed.split(/\s+/u) : [];
|
|
336
411
|
}
|
|
412
|
+
function parseSessionIdFromFileName(sessionFile) {
|
|
413
|
+
const base = basename(sessionFile).replace(/\.jsonl$/iu, "");
|
|
414
|
+
const separator = base.indexOf("_");
|
|
415
|
+
return separator >= 0 ? base.slice(separator + 1) : undefined;
|
|
416
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AgentSessionRuntime, ExtensionCommandContextActions, ExtensionError } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type PixLogDetails, type PixLogLevel } from "../logger.js";
|
|
2
3
|
import type { Entry } from "../types.js";
|
|
3
4
|
export type AppExtensionActionsHost = {
|
|
4
5
|
isRunning(): boolean;
|
|
5
6
|
getInput(): string;
|
|
6
7
|
setInput(value: string): void;
|
|
8
|
+
awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
|
|
7
9
|
resetSessionView(): void;
|
|
8
10
|
loadSessionHistory(): void;
|
|
9
11
|
afterSessionReplacement(message?: string): void;
|
|
@@ -13,9 +15,11 @@ export type AppExtensionActionsHost = {
|
|
|
13
15
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
14
16
|
render(): void;
|
|
15
17
|
};
|
|
18
|
+
export type ExtensionErrorLogger = (level: PixLogLevel, event: string, details?: PixLogDetails) => void;
|
|
16
19
|
export declare class AppExtensionActionsController {
|
|
17
20
|
private readonly host;
|
|
18
|
-
|
|
21
|
+
private readonly logExtensionError;
|
|
22
|
+
constructor(host: AppExtensionActionsHost, logExtensionError?: ExtensionErrorLogger);
|
|
19
23
|
createCommandContextActions(runtime: AgentSessionRuntime): ExtensionCommandContextActions;
|
|
20
24
|
waitForSessionIdle(runtime: AgentSessionRuntime): Promise<void>;
|
|
21
25
|
handleExtensionError(error: ExtensionError): void;
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { createId } from "../id.js";
|
|
3
|
+
import { logPixEvent } from "../logger.js";
|
|
2
4
|
export class AppExtensionActionsController {
|
|
3
5
|
host;
|
|
4
|
-
|
|
6
|
+
logExtensionError;
|
|
7
|
+
constructor(host, logExtensionError = logPixEvent) {
|
|
5
8
|
this.host = host;
|
|
9
|
+
this.logExtensionError = logExtensionError;
|
|
6
10
|
}
|
|
7
11
|
createCommandContextActions(runtime) {
|
|
8
12
|
return {
|
|
9
13
|
waitForIdle: () => this.waitForSessionIdle(runtime),
|
|
10
14
|
newSession: async (options) => {
|
|
15
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
11
16
|
const result = await runtime.newSession(options);
|
|
12
17
|
if (!result.cancelled)
|
|
13
18
|
this.host.afterSessionReplacement("Started a new session.");
|
|
14
19
|
return result;
|
|
15
20
|
},
|
|
16
21
|
fork: async (entryId, options) => {
|
|
22
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
17
23
|
const result = await runtime.fork(entryId, options);
|
|
18
24
|
if (!result.cancelled)
|
|
19
25
|
this.host.afterSessionReplacement("Forked to a new session.");
|
|
@@ -32,12 +38,14 @@ export class AppExtensionActionsController {
|
|
|
32
38
|
return result;
|
|
33
39
|
},
|
|
34
40
|
switchSession: async (sessionPath, options) => {
|
|
41
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
35
42
|
const result = await runtime.switchSession(sessionPath, options);
|
|
36
43
|
if (!result.cancelled)
|
|
37
44
|
this.host.afterSessionReplacement(`Switched session: ${sessionPath}`);
|
|
38
45
|
return result;
|
|
39
46
|
},
|
|
40
47
|
reload: async () => {
|
|
48
|
+
await this.host.awaitCurrentSessionExtensions(runtime);
|
|
41
49
|
await runtime.session.reload();
|
|
42
50
|
this.host.setSessionStatus(runtime.session);
|
|
43
51
|
this.host.showToast("Reloaded resources", "success");
|
|
@@ -51,10 +59,35 @@ export class AppExtensionActionsController {
|
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
handleExtensionError(error) {
|
|
62
|
+
const sourceText = formatExtensionErrorSource(error.extensionPath);
|
|
54
63
|
const pathText = error.extensionPath ? ` (${error.extensionPath})` : "";
|
|
55
|
-
this.
|
|
64
|
+
this.logExtensionError("error", "extension:error", extensionErrorLogDetails(error));
|
|
65
|
+
this.host.addEntry({
|
|
66
|
+
id: createId("error"),
|
|
67
|
+
kind: "error",
|
|
68
|
+
text: `Extension ${error.event} failed${sourceText}${pathText}: ${error.error}`,
|
|
69
|
+
});
|
|
56
70
|
this.host.showToast(`Extension ${error.event} failed`, "error");
|
|
57
71
|
if (this.host.isRunning())
|
|
58
72
|
this.host.render();
|
|
59
73
|
}
|
|
60
74
|
}
|
|
75
|
+
function formatExtensionErrorSource(extensionPath) {
|
|
76
|
+
if (!extensionPath)
|
|
77
|
+
return "";
|
|
78
|
+
const extensionName = basename(extensionPath);
|
|
79
|
+
return extensionName && extensionName !== extensionPath ? ` [${extensionName}]` : "";
|
|
80
|
+
}
|
|
81
|
+
function extensionErrorLogDetails(error) {
|
|
82
|
+
const details = {
|
|
83
|
+
event: error.event,
|
|
84
|
+
error: error.error,
|
|
85
|
+
};
|
|
86
|
+
if (error.extensionPath) {
|
|
87
|
+
details.extensionPath = error.extensionPath;
|
|
88
|
+
details.extensionName = basename(error.extensionPath);
|
|
89
|
+
}
|
|
90
|
+
if (error.stack)
|
|
91
|
+
details.stack = error.stack;
|
|
92
|
+
return details;
|
|
93
|
+
}
|
|
@@ -46,6 +46,8 @@ export declare class AppInputController {
|
|
|
46
46
|
private consumeCommandArrowPageMatch;
|
|
47
47
|
private consumeTerminalEditShortcutSequence;
|
|
48
48
|
private consumeIgnoredModifiedKeySequence;
|
|
49
|
+
private consumeModifiedArrowKeySequence;
|
|
50
|
+
private consumeEscapeKeySequence;
|
|
49
51
|
private consumeClipboardImagePasteSequence;
|
|
50
52
|
private consumeShiftEnterSequence;
|
|
51
53
|
private consumeTerminalInterruptSequence;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InputPasteHandler } from "./input-paste-handler.js";
|
|
2
2
|
import { hasTerminalCommandModifier, isNativeCommandPressed, isNativeShiftPressed } from "./native-modifiers.js";
|
|
3
|
-
import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
|
|
3
|
+
import { parseTerminalEditShortcutSequence, parseTerminalInterruptSequence, parseTerminalModifiedKeySequence, terminalKeyArrowDirection, terminalEditShortcutForControlChar, terminalKeyIsClipboardImagePaste, terminalKeyIsEscape, terminalKeyIsShiftEnter, terminalKeyShouldIgnore, } from "./terminal-edit-shortcuts.js";
|
|
4
4
|
const SHIFT_ENTER_ESCAPE_SEQUENCES = ["\x1b\r", "\x1b\n"];
|
|
5
5
|
export class AppInputController {
|
|
6
6
|
host;
|
|
@@ -112,6 +112,16 @@ export class AppInputController {
|
|
|
112
112
|
continue;
|
|
113
113
|
if (terminalEditShortcutSequence === "pending")
|
|
114
114
|
return;
|
|
115
|
+
const modifiedArrowKeySequence = this.consumeModifiedArrowKeySequence();
|
|
116
|
+
if (modifiedArrowKeySequence === "consumed")
|
|
117
|
+
continue;
|
|
118
|
+
if (modifiedArrowKeySequence === "pending")
|
|
119
|
+
return;
|
|
120
|
+
const escapeKeySequence = this.consumeEscapeKeySequence();
|
|
121
|
+
if (escapeKeySequence === "consumed")
|
|
122
|
+
continue;
|
|
123
|
+
if (escapeKeySequence === "pending")
|
|
124
|
+
return;
|
|
115
125
|
const ignoredModifiedKeySequence = this.consumeIgnoredModifiedKeySequence();
|
|
116
126
|
if (ignoredModifiedKeySequence === "consumed")
|
|
117
127
|
continue;
|
|
@@ -225,7 +235,10 @@ export class AppInputController {
|
|
|
225
235
|
}
|
|
226
236
|
if (/^\x1b\[(?:8|127);\d*$/.test(this.inputBuffer))
|
|
227
237
|
return "pending";
|
|
228
|
-
|
|
238
|
+
// modifyOtherKeys backspace looks like \x1b[27;<mod>;(8|127)~ (two semicolons, no colons).
|
|
239
|
+
// Kitty key sequences also begin with \x1b[27; (e.g. the ESC release \x1b[27;1:3u), so only
|
|
240
|
+
// treat the buffer as a pending modifyOtherKeys partial when it matches that exact shape.
|
|
241
|
+
if (/^\x1b\[27;\d+;\d*$/.test(this.inputBuffer))
|
|
229
242
|
return "pending";
|
|
230
243
|
return "none";
|
|
231
244
|
}
|
|
@@ -277,6 +290,41 @@ export class AppInputController {
|
|
|
277
290
|
this.inputBuffer = this.inputBuffer.slice(result.key.length);
|
|
278
291
|
return "consumed";
|
|
279
292
|
}
|
|
293
|
+
consumeModifiedArrowKeySequence() {
|
|
294
|
+
const result = parseTerminalModifiedKeySequence(this.inputBuffer);
|
|
295
|
+
if (result.kind === "pending")
|
|
296
|
+
return "pending";
|
|
297
|
+
if (result.kind === "none")
|
|
298
|
+
return "none";
|
|
299
|
+
const direction = terminalKeyArrowDirection(result.key);
|
|
300
|
+
if (!direction)
|
|
301
|
+
return "none";
|
|
302
|
+
this.inputBuffer = this.inputBuffer.slice(result.key.length);
|
|
303
|
+
if (terminalKeyShouldIgnore(result.key))
|
|
304
|
+
return "consumed";
|
|
305
|
+
if (direction === "up")
|
|
306
|
+
this.handleArrowUp();
|
|
307
|
+
else if (direction === "down")
|
|
308
|
+
this.handleArrowDown();
|
|
309
|
+
else if (direction === "right")
|
|
310
|
+
this.handleArrowRight();
|
|
311
|
+
else
|
|
312
|
+
this.handleArrowLeft();
|
|
313
|
+
return "consumed";
|
|
314
|
+
}
|
|
315
|
+
consumeEscapeKeySequence() {
|
|
316
|
+
const result = parseTerminalModifiedKeySequence(this.inputBuffer);
|
|
317
|
+
if (result.kind === "pending")
|
|
318
|
+
return "pending";
|
|
319
|
+
if (result.kind === "none")
|
|
320
|
+
return "none";
|
|
321
|
+
if (!terminalKeyIsEscape(result.key))
|
|
322
|
+
return "none";
|
|
323
|
+
this.inputBuffer = this.inputBuffer.slice(result.key.length);
|
|
324
|
+
if (!terminalKeyShouldIgnore(result.key))
|
|
325
|
+
void this.host.handleEscape();
|
|
326
|
+
return "consumed";
|
|
327
|
+
}
|
|
280
328
|
consumeClipboardImagePasteSequence() {
|
|
281
329
|
const result = parseTerminalModifiedKeySequence(this.inputBuffer);
|
|
282
330
|
if (result.kind === "pending")
|
|
@@ -39,5 +39,7 @@ export declare function parseTerminalInterruptSequence(input: string): {
|
|
|
39
39
|
export declare function terminalKeyIsShiftEnter(key: ParsedModifiedKey): boolean;
|
|
40
40
|
export declare function terminalKeyIsClipboardImagePaste(key: ParsedModifiedKey): boolean;
|
|
41
41
|
export declare function terminalKeyShouldIgnore(key: ParsedModifiedKey): boolean;
|
|
42
|
+
export declare function terminalKeyIsEscape(key: ParsedModifiedKey): boolean;
|
|
43
|
+
export declare function terminalKeyArrowDirection(key: ParsedModifiedKey): "up" | "down" | "right" | "left" | undefined;
|
|
42
44
|
export declare function terminalEditShortcutForControlChar(char: string, shiftPressed: boolean): TerminalEditShortcut | undefined;
|
|
43
45
|
export {};
|