pi-ui-extend 0.1.13 → 0.1.15
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 +1 -1
- package/dist/app/app.d.ts +5 -0
- package/dist/app/app.js +82 -12
- package/dist/app/commands/command-controller.js +1 -0
- package/dist/app/commands/command-host.d.ts +3 -0
- package/dist/app/commands/command-model-actions.d.ts +2 -0
- package/dist/app/commands/command-model-actions.js +40 -4
- 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/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +8 -2
- package/dist/app/logger.d.ts +25 -0
- package/dist/app/logger.js +90 -0
- package/dist/app/model/model-usage-status.js +30 -15
- package/dist/app/popup/menu-items-controller.d.ts +2 -0
- package/dist/app/popup/menu-items-controller.js +45 -6
- package/dist/app/popup/popup-action-controller.d.ts +2 -1
- package/dist/app/popup/popup-action-controller.js +7 -4
- package/dist/app/popup/popup-menu-controller.d.ts +36 -23
- package/dist/app/popup/popup-menu-controller.js +68 -322
- package/dist/app/rendering/conversation-entry-renderer.js +3 -3
- package/dist/app/rendering/conversation-viewport.d.ts +10 -2
- package/dist/app/rendering/conversation-viewport.js +157 -16
- package/dist/app/rendering/editor-panels.js +4 -2
- package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
- package/dist/app/rendering/popup-menu-renderer.js +307 -0
- package/dist/app/rendering/render-controller.js +5 -13
- package/dist/app/rendering/status-line-renderer.d.ts +1 -1
- package/dist/app/rendering/status-line-renderer.js +27 -24
- package/dist/app/rendering/toast-controller.d.ts +11 -3
- package/dist/app/rendering/toast-controller.js +53 -12
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +20 -10
- package/dist/app/screen/mouse-controller.d.ts +2 -2
- package/dist/app/screen/mouse-controller.js +27 -48
- package/dist/app/screen/screen-styler.d.ts +1 -1
- package/dist/app/screen/screen-styler.js +9 -7
- package/dist/app/screen/scroll-controller.d.ts +11 -9
- package/dist/app/screen/scroll-controller.js +50 -45
- package/dist/app/session/lazy-session-manager.d.ts +11 -0
- package/dist/app/session/lazy-session-manager.js +539 -0
- package/dist/app/session/pix-system-message.d.ts +16 -0
- package/dist/app/session/pix-system-message.js +64 -0
- package/dist/app/session/session-event-controller.d.ts +11 -0
- package/dist/app/session/session-event-controller.js +58 -2
- package/dist/app/session/session-history.d.ts +18 -0
- package/dist/app/session/session-history.js +72 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
- package/dist/app/session/session-lifecycle-controller.js +7 -2
- package/dist/app/session/tabs-controller.d.ts +13 -1
- package/dist/app/session/tabs-controller.js +248 -27
- package/dist/app/todo/todo-model.d.ts +3 -1
- package/dist/app/todo/todo-model.js +14 -2
- package/dist/app/types.d.ts +5 -2
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +12 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.js +73 -25
- package/dist/default-pix-config.js +2 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
- package/dist/schemas/pi-tools-suite-schema.js +1 -0
- package/dist/schemas/pix-schema.d.ts +2 -1
- package/dist/schemas/pix-schema.js +5 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/external/pi-tools-suite/README.md +1 -0
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
- package/external/pi-tools-suite/src/config.ts +8 -0
- package/external/pi-tools-suite/src/dcp/index.ts +16 -1
- package/external/pi-tools-suite/src/dcp/state.ts +35 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
- package/external/pi-tools-suite/src/todo/index.ts +181 -11
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
- package/external/pi-tools-suite/src/todo/todo.ts +10 -5
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
- package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +6 -2
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { getAgentDir,
|
|
3
|
+
import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import { getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { isRecord } from "../guards.js";
|
|
7
7
|
import { createId } from "../id.js";
|
|
8
8
|
import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-info.js";
|
|
9
9
|
import { tabPanelRows } from "../rendering/tab-line-renderer.js";
|
|
10
10
|
const TAB_STATE_VERSION = 3;
|
|
11
11
|
const MAX_RESTORED_TABS = 8;
|
|
12
|
+
const MAX_PROJECT_SESSIONS = 20;
|
|
12
13
|
const TAB_ATTENTION_BLINK_KEY = "tab-attention";
|
|
14
|
+
const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
|
|
13
15
|
export class AppTabsController {
|
|
14
16
|
host;
|
|
15
17
|
tabItems = [];
|
|
@@ -21,6 +23,8 @@ export class AppTabsController {
|
|
|
21
23
|
pendingActiveTabId;
|
|
22
24
|
historyLoadGeneration = 0;
|
|
23
25
|
restored = false;
|
|
26
|
+
retentionCleanupRunning = false;
|
|
27
|
+
retentionCleanupScheduled = false;
|
|
24
28
|
constructor(host) {
|
|
25
29
|
this.host = host;
|
|
26
30
|
}
|
|
@@ -153,17 +157,21 @@ export class AppTabsController {
|
|
|
153
157
|
if (!runtime)
|
|
154
158
|
return;
|
|
155
159
|
this.syncActiveTabFromRuntime({ save: false });
|
|
156
|
-
if (this.host.options.noSession)
|
|
160
|
+
if (this.host.options.noSession) {
|
|
161
|
+
this.settleStartupTabPlaceholders();
|
|
157
162
|
return;
|
|
163
|
+
}
|
|
158
164
|
const saved = await this.loadTabs();
|
|
159
165
|
if (!saved || saved.tabs.length === 0) {
|
|
166
|
+
this.settleStartupTabPlaceholders();
|
|
160
167
|
await this.saveTabs();
|
|
161
168
|
return;
|
|
162
169
|
}
|
|
163
|
-
const
|
|
164
|
-
const restoredTabs = this.restoredTabs(saved, sessionTitles);
|
|
170
|
+
const restoredTabs = this.restoredTabs(saved);
|
|
165
171
|
if (restoredTabs.length === 0) {
|
|
172
|
+
this.settleStartupTabPlaceholders();
|
|
166
173
|
await this.saveTabs();
|
|
174
|
+
this.scheduleProjectSessionRetention();
|
|
167
175
|
return;
|
|
168
176
|
}
|
|
169
177
|
const currentPath = runtime.session.sessionFile ? resolve(runtime.session.sessionFile) : undefined;
|
|
@@ -180,35 +188,41 @@ export class AppTabsController {
|
|
|
180
188
|
if (explicitSessionPath && currentPath)
|
|
181
189
|
this.ensureCurrentSessionTab(runtime.session);
|
|
182
190
|
if (!desiredPath) {
|
|
191
|
+
this.settleStartupTabPlaceholders();
|
|
183
192
|
await this.saveTabs();
|
|
193
|
+
this.scheduleProjectSessionRetention();
|
|
184
194
|
return;
|
|
185
195
|
}
|
|
196
|
+
let restoredRuntime = runtime;
|
|
186
197
|
if (currentPath !== desiredPath) {
|
|
187
198
|
this.host.setStatus("restoring tabs");
|
|
188
199
|
this.host.render();
|
|
189
200
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error("restore cancelled");
|
|
201
|
+
restoredRuntime = await this.host.createRuntimeForSession(desiredPath);
|
|
202
|
+
await this.host.activateRuntime(restoredRuntime);
|
|
193
203
|
}
|
|
194
204
|
catch {
|
|
195
205
|
this.host.showToast("Could not restore the previous active tab", "warning");
|
|
196
206
|
this.replaceTabs([this.tabFromSession(runtime.session), ...restoredTabs], currentPath);
|
|
197
207
|
this.storeActiveRuntime(runtime);
|
|
208
|
+
this.settleStartupTabPlaceholders();
|
|
198
209
|
await this.saveTabs();
|
|
210
|
+
this.scheduleProjectSessionRetention();
|
|
199
211
|
return;
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
this.syncActiveTabFromRuntime({ save: false });
|
|
215
|
+
this.settleStartupTabPlaceholders();
|
|
203
216
|
this.host.resetSessionView();
|
|
204
217
|
if (this.activeTabId)
|
|
205
218
|
this.restoreDeferredUserMessages(this.activeTabId);
|
|
206
219
|
this.host.loadSessionHistory();
|
|
207
|
-
this.host.setSessionStatus(
|
|
208
|
-
this.host.setSessionActivity(this.sessionActivity(
|
|
220
|
+
this.host.setSessionStatus(restoredRuntime.session);
|
|
221
|
+
this.host.setSessionActivity(this.sessionActivity(restoredRuntime.session));
|
|
209
222
|
if (this.activeTabId)
|
|
210
223
|
this.restoreInputState(this.activeTabId);
|
|
211
224
|
await this.saveTabs();
|
|
225
|
+
this.scheduleProjectSessionRetention();
|
|
212
226
|
}
|
|
213
227
|
async openNewTab() {
|
|
214
228
|
if (this.pendingActiveTabId) {
|
|
@@ -237,6 +251,7 @@ export class AppTabsController {
|
|
|
237
251
|
this.updateTabFromSession(tab, newRuntime.session);
|
|
238
252
|
this.setRuntimeForTab(tab.id, newRuntime);
|
|
239
253
|
this.restoreInputState(tab.id);
|
|
254
|
+
this.host.closeMenusForTabSwitch?.();
|
|
240
255
|
try {
|
|
241
256
|
await this.host.activateRuntime(newRuntime);
|
|
242
257
|
}
|
|
@@ -245,6 +260,7 @@ export class AppTabsController {
|
|
|
245
260
|
this.pendingActiveTabId = undefined;
|
|
246
261
|
}
|
|
247
262
|
void this.saveTabs();
|
|
263
|
+
this.scheduleProjectSessionRetention();
|
|
248
264
|
this.host.resetSessionView();
|
|
249
265
|
this.restoreDeferredUserMessages(tab.id);
|
|
250
266
|
if (isEmptyStartupSession(newRuntime)) {
|
|
@@ -307,6 +323,7 @@ export class AppTabsController {
|
|
|
307
323
|
this.updateTabFromSession(tab, newRuntime.session);
|
|
308
324
|
this.setRuntimeForTab(tab.id, newRuntime);
|
|
309
325
|
this.restoreInputState(tab.id);
|
|
326
|
+
this.host.closeMenusForTabSwitch?.();
|
|
310
327
|
try {
|
|
311
328
|
await this.host.activateRuntime(newRuntime);
|
|
312
329
|
}
|
|
@@ -316,6 +333,7 @@ export class AppTabsController {
|
|
|
316
333
|
this.activeTabId = previousTabId;
|
|
317
334
|
if (previousTabId)
|
|
318
335
|
this.restoreInputState(previousTabId);
|
|
336
|
+
this.host.closeMenusForTabSwitch?.();
|
|
319
337
|
if (this.host.runtime() !== previousRuntime) {
|
|
320
338
|
try {
|
|
321
339
|
await this.host.activateRuntime(previousRuntime);
|
|
@@ -346,6 +364,120 @@ export class AppTabsController {
|
|
|
346
364
|
await this.loadActiveSessionHistory(newRuntime);
|
|
347
365
|
return true;
|
|
348
366
|
}
|
|
367
|
+
async forkSessionEntryInNewTab(entryId) {
|
|
368
|
+
if (this.pendingActiveTabId) {
|
|
369
|
+
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const runtime = this.idleRuntime("fork");
|
|
373
|
+
if (!runtime)
|
|
374
|
+
return false;
|
|
375
|
+
if (this.host.options.noSession) {
|
|
376
|
+
this.host.showToast("Fork in new tab is unavailable with --no-session", "warning");
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const currentSessionPath = runtime.session.sessionFile ? resolve(runtime.session.sessionFile) : undefined;
|
|
380
|
+
if (!currentSessionPath) {
|
|
381
|
+
this.host.showToast("Fork in new tab requires a persisted session", "warning");
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
this.cancelHistoryLoad();
|
|
385
|
+
this.syncActiveTabFromRuntime({ save: false });
|
|
386
|
+
this.storeActiveInputState();
|
|
387
|
+
this.storeActiveDeferredUserMessages();
|
|
388
|
+
const previousTabId = this.activeTabId;
|
|
389
|
+
const previousRuntime = runtime;
|
|
390
|
+
this.host.setStatus("forking session tab");
|
|
391
|
+
this.host.render();
|
|
392
|
+
let forkRuntime;
|
|
393
|
+
try {
|
|
394
|
+
forkRuntime = await this.host.createRuntimeForSession(currentSessionPath);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
this.host.showToast("Could not fork in new tab", "warning");
|
|
398
|
+
this.host.setSessionStatus(previousRuntime.session);
|
|
399
|
+
this.host.render();
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
let result;
|
|
403
|
+
try {
|
|
404
|
+
result = await forkRuntime.fork(entryId);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
if (result.cancelled) {
|
|
411
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
412
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: "Fork cancelled." });
|
|
413
|
+
this.host.setSessionStatus(previousRuntime.session);
|
|
414
|
+
this.host.render();
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const existingTab = this.findTabForSession(forkRuntime.session);
|
|
418
|
+
if (existingTab) {
|
|
419
|
+
if (result.selectedText)
|
|
420
|
+
this.inputStatesByTabId.set(existingTab.id, this.inputStateFromText(result.selectedText));
|
|
421
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
422
|
+
await this.switchToTab(existingTab.id);
|
|
423
|
+
this.host.showToast("Fork opened in existing tab", "success");
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
const tab = this.tabFromSession(forkRuntime.session, { titlePlaceholder: "new" });
|
|
427
|
+
this.tabItems.push(tab);
|
|
428
|
+
this.activeTabId = tab.id;
|
|
429
|
+
this.pendingActiveTabId = tab.id;
|
|
430
|
+
this.clearTabAttention(tab);
|
|
431
|
+
this.updateTabFromSession(tab, forkRuntime.session);
|
|
432
|
+
this.setRuntimeForTab(tab.id, forkRuntime);
|
|
433
|
+
if (result.selectedText)
|
|
434
|
+
this.inputStatesByTabId.set(tab.id, this.inputStateFromText(result.selectedText));
|
|
435
|
+
this.restoreInputState(tab.id);
|
|
436
|
+
this.host.closeMenusForTabSwitch?.();
|
|
437
|
+
try {
|
|
438
|
+
await this.host.activateRuntime(forkRuntime);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
this.pendingActiveTabId = undefined;
|
|
442
|
+
this.removeTab(tab.id);
|
|
443
|
+
this.activeTabId = previousTabId;
|
|
444
|
+
if (previousTabId)
|
|
445
|
+
this.restoreInputState(previousTabId);
|
|
446
|
+
this.host.closeMenusForTabSwitch?.();
|
|
447
|
+
if (this.host.runtime() !== previousRuntime) {
|
|
448
|
+
try {
|
|
449
|
+
await this.host.activateRuntime(previousRuntime);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// Keep the best available runtime below and surface the switch failure.
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
void this.host.disposeRuntime(forkRuntime);
|
|
456
|
+
this.host.showToast("Could not open fork tab", "warning");
|
|
457
|
+
this.host.resetSessionView();
|
|
458
|
+
if (previousTabId)
|
|
459
|
+
this.restoreDeferredUserMessages(previousTabId);
|
|
460
|
+
this.host.loadSessionHistory();
|
|
461
|
+
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
462
|
+
this.host.setSessionActivity(this.sessionActivity(this.host.runtime()?.session));
|
|
463
|
+
this.host.render();
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
this.pendingActiveTabId = undefined;
|
|
467
|
+
this.activeTabId = tab.id;
|
|
468
|
+
this.clearTabAttention(tab);
|
|
469
|
+
this.updateTabFromSession(tab, forkRuntime.session);
|
|
470
|
+
this.setRuntimeForTab(tab.id, forkRuntime);
|
|
471
|
+
this.restoreInputState(tab.id);
|
|
472
|
+
void this.saveTabs();
|
|
473
|
+
this.scheduleProjectSessionRetention();
|
|
474
|
+
await this.loadActiveSessionHistory(forkRuntime);
|
|
475
|
+
this.host.addEntry({ id: createId("system"), kind: "system", text: `Forked from entry ${entryId} in a new tab.` });
|
|
476
|
+
this.host.setSessionStatus(forkRuntime.session);
|
|
477
|
+
this.host.showToast("Fork opened in new tab", "success");
|
|
478
|
+
this.host.render();
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
349
481
|
async switchToTab(tabId) {
|
|
350
482
|
if (this.pendingActiveTabId) {
|
|
351
483
|
this.host.showToast("Wait for the tab to finish loading", "info");
|
|
@@ -376,6 +508,7 @@ export class AppTabsController {
|
|
|
376
508
|
target.activity = "thinking";
|
|
377
509
|
this.clearTabAttention(target);
|
|
378
510
|
this.restoreInputState(target.id);
|
|
511
|
+
this.host.closeMenusForTabSwitch?.();
|
|
379
512
|
this.host.resetSessionView();
|
|
380
513
|
this.restoreDeferredUserMessages(target.id);
|
|
381
514
|
this.host.setStatus("switching tab");
|
|
@@ -397,6 +530,7 @@ export class AppTabsController {
|
|
|
397
530
|
this.activeTabId = previousTabId;
|
|
398
531
|
if (previousTabId)
|
|
399
532
|
this.restoreInputState(previousTabId);
|
|
533
|
+
this.host.closeMenusForTabSwitch?.();
|
|
400
534
|
if (this.host.runtime() !== previousRuntime) {
|
|
401
535
|
try {
|
|
402
536
|
await this.host.activateRuntime(previousRuntime);
|
|
@@ -481,6 +615,7 @@ export class AppTabsController {
|
|
|
481
615
|
this.updateTabFromSession(nextTab, nextRuntime.session);
|
|
482
616
|
this.setRuntimeForTab(nextTab.id, nextRuntime);
|
|
483
617
|
this.restoreInputState(nextTab.id);
|
|
618
|
+
this.host.closeMenusForTabSwitch?.();
|
|
484
619
|
void this.host.disposeRuntime(runtime);
|
|
485
620
|
void this.saveTabs();
|
|
486
621
|
await this.loadActiveSessionHistory(nextRuntime);
|
|
@@ -509,6 +644,7 @@ export class AppTabsController {
|
|
|
509
644
|
this.inputStatesByTabId.delete(tab.id);
|
|
510
645
|
this.deferredUserMessagesByTabId.delete(tab.id);
|
|
511
646
|
this.restoreInputState(tab.id);
|
|
647
|
+
this.host.closeMenusForTabSwitch?.();
|
|
512
648
|
this.stopAttentionBlinkIfIdle();
|
|
513
649
|
this.host.resetSessionView();
|
|
514
650
|
this.restoreDeferredUserMessages(tab.id);
|
|
@@ -522,6 +658,7 @@ export class AppTabsController {
|
|
|
522
658
|
this.host.setSessionStatus(runtime.session);
|
|
523
659
|
this.host.setSessionActivity(this.sessionActivity(runtime.session));
|
|
524
660
|
void this.saveTabs();
|
|
661
|
+
this.scheduleProjectSessionRetention();
|
|
525
662
|
this.host.render();
|
|
526
663
|
}
|
|
527
664
|
async loadActiveSessionHistory(runtime) {
|
|
@@ -539,6 +676,7 @@ export class AppTabsController {
|
|
|
539
676
|
if (!isCancelled())
|
|
540
677
|
this.host.render();
|
|
541
678
|
},
|
|
679
|
+
lazyOlderHistory: true,
|
|
542
680
|
});
|
|
543
681
|
if (!completed || isCancelled())
|
|
544
682
|
return;
|
|
@@ -583,6 +721,12 @@ export class AppTabsController {
|
|
|
583
721
|
activeTab() {
|
|
584
722
|
return this.activeTabId ? this.tabItems.find((tab) => tab.id === this.activeTabId) : undefined;
|
|
585
723
|
}
|
|
724
|
+
settleStartupTabPlaceholders() {
|
|
725
|
+
for (const tab of this.tabItems) {
|
|
726
|
+
if (tab.titlePlaceholder === "loading")
|
|
727
|
+
tab.titlePlaceholder = "new";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
586
730
|
storeActiveRuntime(runtime = this.host.runtime()) {
|
|
587
731
|
if (!this.activeTabId || !runtime)
|
|
588
732
|
return;
|
|
@@ -662,6 +806,9 @@ export class AppTabsController {
|
|
|
662
806
|
restoreInputState(tabId) {
|
|
663
807
|
this.host.restoreInputState(this.inputStatesByTabId.get(tabId) ?? { text: "", cursor: 0 });
|
|
664
808
|
}
|
|
809
|
+
inputStateFromText(text) {
|
|
810
|
+
return { text, cursor: text.length };
|
|
811
|
+
}
|
|
665
812
|
restoreDeferredUserMessages(tabId) {
|
|
666
813
|
this.host.restoreDeferredUserMessages?.(this.deferredUserMessagesByTabId.get(tabId) ?? []);
|
|
667
814
|
}
|
|
@@ -712,6 +859,7 @@ export class AppTabsController {
|
|
|
712
859
|
this.tabItems.push({
|
|
713
860
|
id: tab.id,
|
|
714
861
|
title: tab.title,
|
|
862
|
+
...(tab.titlePlaceholder ? { titlePlaceholder: tab.titlePlaceholder } : {}),
|
|
715
863
|
status: "waiting",
|
|
716
864
|
activity: tab.activity ?? "idle",
|
|
717
865
|
...(sessionPath ? { sessionPath } : {}),
|
|
@@ -800,8 +948,11 @@ export class AppTabsController {
|
|
|
800
948
|
return session.sessionFile ? resolve(session.sessionFile) : undefined;
|
|
801
949
|
}
|
|
802
950
|
sessionTitle(session) {
|
|
803
|
-
|
|
804
|
-
|
|
951
|
+
return this.sessionTitleFromParts(session.sessionId, session.sessionName);
|
|
952
|
+
}
|
|
953
|
+
sessionTitleFromParts(sessionId, sessionName) {
|
|
954
|
+
const name = sessionName?.trim();
|
|
955
|
+
return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : `session ${sessionId.slice(0, 8)}`;
|
|
805
956
|
}
|
|
806
957
|
sessionActivity(session) {
|
|
807
958
|
return session?.isStreaming || session?.isCompacting ? "running" : "idle";
|
|
@@ -833,19 +984,7 @@ export class AppTabsController {
|
|
|
833
984
|
initialVisible: true,
|
|
834
985
|
});
|
|
835
986
|
}
|
|
836
|
-
|
|
837
|
-
try {
|
|
838
|
-
const sessions = await SessionManager.list(this.host.options.cwd);
|
|
839
|
-
return new Map(sessions.map((session) => [
|
|
840
|
-
resolve(session.path),
|
|
841
|
-
(session.name?.trim() || `session ${session.id.slice(0, 8)}`),
|
|
842
|
-
]));
|
|
843
|
-
}
|
|
844
|
-
catch {
|
|
845
|
-
return new Map();
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
restoredTabs(saved, titles) {
|
|
987
|
+
restoredTabs(saved) {
|
|
849
988
|
const tabs = [];
|
|
850
989
|
const seen = new Set();
|
|
851
990
|
for (const tab of saved.tabs) {
|
|
@@ -855,10 +994,13 @@ export class AppTabsController {
|
|
|
855
994
|
if (seen.has(sessionPath) || (!existsSync(sessionPath) && !hasDraftInput && !hasDeferredQueue))
|
|
856
995
|
continue;
|
|
857
996
|
seen.add(sessionPath);
|
|
858
|
-
const
|
|
997
|
+
const savedTitle = tab.title?.trim();
|
|
998
|
+
const restoredLoadingTitle = savedTitle !== undefined && LOADING_TAB_TITLE_PATTERN.test(savedTitle);
|
|
999
|
+
const title = restoredLoadingTitle ? this.defaultSessionTitleFromPath(sessionPath) : savedTitle;
|
|
859
1000
|
tabs.push({
|
|
860
1001
|
id: createId("tab"),
|
|
861
1002
|
title: title || "session",
|
|
1003
|
+
...(restoredLoadingTitle ? { titlePlaceholder: "new" } : {}),
|
|
862
1004
|
status: "waiting",
|
|
863
1005
|
sessionPath,
|
|
864
1006
|
});
|
|
@@ -867,6 +1009,12 @@ export class AppTabsController {
|
|
|
867
1009
|
}
|
|
868
1010
|
return tabs;
|
|
869
1011
|
}
|
|
1012
|
+
defaultSessionTitleFromPath(sessionPath) {
|
|
1013
|
+
const fileName = basename(sessionPath, extname(sessionPath));
|
|
1014
|
+
const sessionId = /^[0-9a-f]{8}/iu.exec(fileName)?.[0]?.toLowerCase()
|
|
1015
|
+
?? createHash("sha256").update(sessionPath).digest("hex").slice(0, 8);
|
|
1016
|
+
return `session ${sessionId}`;
|
|
1017
|
+
}
|
|
870
1018
|
async loadTabs() {
|
|
871
1019
|
try {
|
|
872
1020
|
const raw = await readFile(this.filePath(), "utf8");
|
|
@@ -978,4 +1126,77 @@ export class AppTabsController {
|
|
|
978
1126
|
const key = createHash("sha256").update(resolve(this.host.options.cwd)).digest("hex").slice(0, 24);
|
|
979
1127
|
return join(getAgentDir(), "pix", "tabs", `${key}.json`);
|
|
980
1128
|
}
|
|
1129
|
+
sessionDir() {
|
|
1130
|
+
const safePath = `--${resolve(this.host.options.cwd).replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
1131
|
+
return join(getAgentDir(), "sessions", safePath);
|
|
1132
|
+
}
|
|
1133
|
+
scheduleProjectSessionRetention() {
|
|
1134
|
+
if (this.host.options.noSession || this.retentionCleanupScheduled || this.retentionCleanupRunning)
|
|
1135
|
+
return;
|
|
1136
|
+
this.retentionCleanupScheduled = true;
|
|
1137
|
+
setTimeout(() => {
|
|
1138
|
+
this.retentionCleanupScheduled = false;
|
|
1139
|
+
void this.cleanupOldProjectSessions();
|
|
1140
|
+
}, 0);
|
|
1141
|
+
}
|
|
1142
|
+
async cleanupOldProjectSessions() {
|
|
1143
|
+
if (this.retentionCleanupRunning)
|
|
1144
|
+
return;
|
|
1145
|
+
this.retentionCleanupRunning = true;
|
|
1146
|
+
try {
|
|
1147
|
+
const sessionDir = this.sessionDir();
|
|
1148
|
+
const preserved = this.preservedSessionPaths();
|
|
1149
|
+
const entries = await readdir(sessionDir, { withFileTypes: true });
|
|
1150
|
+
const sessions = [];
|
|
1151
|
+
for (const entry of entries) {
|
|
1152
|
+
if (!entry.isFile() || extname(entry.name) !== ".jsonl")
|
|
1153
|
+
continue;
|
|
1154
|
+
const path = resolve(sessionDir, entry.name);
|
|
1155
|
+
try {
|
|
1156
|
+
const info = await stat(path);
|
|
1157
|
+
sessions.push({ path, modifiedMs: info.mtimeMs });
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
// Ignore files that disappear while cleanup is scanning.
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (sessions.length <= MAX_PROJECT_SESSIONS)
|
|
1164
|
+
return;
|
|
1165
|
+
sessions.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
1166
|
+
const keep = new Set(preserved);
|
|
1167
|
+
for (const session of sessions) {
|
|
1168
|
+
if (keep.size >= MAX_PROJECT_SESSIONS)
|
|
1169
|
+
break;
|
|
1170
|
+
keep.add(session.path);
|
|
1171
|
+
}
|
|
1172
|
+
for (const session of sessions) {
|
|
1173
|
+
if (keep.has(session.path))
|
|
1174
|
+
continue;
|
|
1175
|
+
try {
|
|
1176
|
+
await unlink(session.path);
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
// Session retention must never interrupt the terminal UI.
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
// Session retention must never interrupt the terminal UI.
|
|
1185
|
+
}
|
|
1186
|
+
finally {
|
|
1187
|
+
this.retentionCleanupRunning = false;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
preservedSessionPaths() {
|
|
1191
|
+
const preserved = new Set();
|
|
1192
|
+
const add = (sessionPath) => {
|
|
1193
|
+
if (sessionPath)
|
|
1194
|
+
preserved.add(resolve(sessionPath));
|
|
1195
|
+
};
|
|
1196
|
+
add(this.host.options.sessionPath);
|
|
1197
|
+
add(this.host.runtime()?.session.sessionFile);
|
|
1198
|
+
for (const tab of this.tabItems)
|
|
1199
|
+
add(tab.sessionPath);
|
|
1200
|
+
return preserved;
|
|
1201
|
+
}
|
|
981
1202
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { StyledSegment, TodoAction, TodoDetails, TodoLiveStateEvent, TodoPriority, TodoStatus, TodoTask, TodoTaskLinePart, TodoTaskRow } from "../types.js";
|
|
1
|
+
import type { StyledSegment, ThinkingLevel, TodoAction, TodoDetails, TodoLiveStateEvent, TodoPriority, TodoStatus, TodoTask, TodoTaskLinePart, TodoTaskRow } from "../types.js";
|
|
2
2
|
export declare function isTodoAction(value: unknown): value is TodoAction;
|
|
3
3
|
export declare function isTodoStatus(value: unknown): value is TodoStatus;
|
|
4
4
|
export declare function isTodoPriority(value: unknown): value is TodoPriority;
|
|
5
|
+
export declare function isTodoThinkingLevel(value: unknown): value is ThinkingLevel;
|
|
5
6
|
export declare function isTodoTask(value: unknown): value is TodoTask;
|
|
6
7
|
export declare function isTodoDetails(value: unknown): value is TodoDetails;
|
|
7
8
|
export declare function isTodoLiveStateEvent(value: unknown): value is TodoLiveStateEvent;
|
|
@@ -17,6 +18,7 @@ export declare function formatTodoTaskLine(task: TodoTask, options?: {
|
|
|
17
18
|
}): string;
|
|
18
19
|
export declare function todoTaskLineSegments(task: TodoTask, mutedColor: string, options?: {
|
|
19
20
|
depth?: number;
|
|
21
|
+
thinkingColor?: (level: ThinkingLevel) => string;
|
|
20
22
|
}): StyledSegment[];
|
|
21
23
|
export declare function shiftSegmentsToSlice(segments: readonly StyledSegment[], start: number, length: number): StyledSegment[];
|
|
22
24
|
export declare function formatTodoPanelStats(tasks: readonly TodoTask[]): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TODO_ACTIONS, TODO_PRIORITIES, TODO_STATUSES } from "../constants.js";
|
|
1
|
+
import { THINKING_LEVELS, TODO_ACTIONS, TODO_PRIORITIES, TODO_STATUSES } from "../constants.js";
|
|
2
2
|
import { isNumberArray, isRecord, isStringArray } from "../guards.js";
|
|
3
3
|
import { APP_ICONS } from "../icons.js";
|
|
4
4
|
export function isTodoAction(value) {
|
|
@@ -10,6 +10,9 @@ export function isTodoStatus(value) {
|
|
|
10
10
|
export function isTodoPriority(value) {
|
|
11
11
|
return typeof value === "string" && TODO_PRIORITIES.includes(value);
|
|
12
12
|
}
|
|
13
|
+
export function isTodoThinkingLevel(value) {
|
|
14
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
15
|
+
}
|
|
13
16
|
export function isTodoTask(value) {
|
|
14
17
|
if (!isRecord(value))
|
|
15
18
|
return false;
|
|
@@ -25,6 +28,8 @@ export function isTodoTask(value) {
|
|
|
25
28
|
return false;
|
|
26
29
|
if (value.priority !== undefined && !isTodoPriority(value.priority))
|
|
27
30
|
return false;
|
|
31
|
+
if (value.thinking !== undefined && !isTodoThinkingLevel(value.thinking))
|
|
32
|
+
return false;
|
|
28
33
|
if (value.parentId !== undefined && typeof value.parentId !== "number")
|
|
29
34
|
return false;
|
|
30
35
|
if (value.blockedBy !== undefined && !isNumberArray(value.blockedBy))
|
|
@@ -121,6 +126,8 @@ export function todoTaskLineParts(task, options = {}) {
|
|
|
121
126
|
parts.push({ text: `— ${task.activeForm}` });
|
|
122
127
|
if (task.priority)
|
|
123
128
|
parts.push({ text: `(${task.priority})`, muted: true });
|
|
129
|
+
if (task.thinking)
|
|
130
|
+
parts.push({ text: `[${task.thinking}]`, thinking: task.thinking });
|
|
124
131
|
if (task.parentId !== undefined)
|
|
125
132
|
parts.push({ text: `parent:#${task.parentId}` });
|
|
126
133
|
if (task.blockedBy && task.blockedBy.length > 0)
|
|
@@ -141,7 +148,12 @@ export function todoTaskLineSegments(task, mutedColor, options = {}) {
|
|
|
141
148
|
const start = offset;
|
|
142
149
|
const end = start + part.text.length;
|
|
143
150
|
const segment = { start, end };
|
|
144
|
-
if (part.
|
|
151
|
+
if (part.thinking) {
|
|
152
|
+
const foreground = options.thinkingColor?.(part.thinking);
|
|
153
|
+
if (foreground)
|
|
154
|
+
segment.foreground = foreground;
|
|
155
|
+
}
|
|
156
|
+
else if (part.muted)
|
|
145
157
|
segment.foreground = mutedColor;
|
|
146
158
|
if (task.status === "completed" && index > 0)
|
|
147
159
|
segment.strikethrough = true;
|
package/dist/app/types.d.ts
CHANGED
|
@@ -100,6 +100,7 @@ export type TodoTask = {
|
|
|
100
100
|
description?: string;
|
|
101
101
|
activeForm?: string;
|
|
102
102
|
priority?: TodoPriority;
|
|
103
|
+
thinking?: ThinkingLevel;
|
|
103
104
|
parentId?: number;
|
|
104
105
|
blockedBy?: number[];
|
|
105
106
|
tags?: string[];
|
|
@@ -109,6 +110,7 @@ export type TodoTask = {
|
|
|
109
110
|
export type TodoTaskLinePart = {
|
|
110
111
|
text: string;
|
|
111
112
|
muted?: boolean;
|
|
113
|
+
thinking?: ThinkingLevel;
|
|
112
114
|
};
|
|
113
115
|
export type TodoTaskRow = {
|
|
114
116
|
task: TodoTask;
|
|
@@ -498,9 +500,10 @@ export type ThinkingMenuValue = {
|
|
|
498
500
|
level: ThinkingLevel;
|
|
499
501
|
current: boolean;
|
|
500
502
|
};
|
|
501
|
-
export type UserMessageMenuValue = "copy" | "fork" | "undo";
|
|
503
|
+
export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
|
|
502
504
|
export type UserMessageJumpMenuValue = {
|
|
503
|
-
entryId
|
|
505
|
+
entryId?: string;
|
|
506
|
+
sessionEntryId?: string;
|
|
504
507
|
};
|
|
505
508
|
export type QueueMessageMenuValue = "cancel" | "edit" | "send-now";
|
|
506
509
|
export type ResumeMenuValue = {
|
|
@@ -18,6 +18,7 @@ export type AppWorkspaceActionsControllerHost = {
|
|
|
18
18
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
19
19
|
render(): void;
|
|
20
20
|
isRunning(): boolean;
|
|
21
|
+
forkSessionEntryInNewTab(sessionEntryId: string): Promise<boolean>;
|
|
21
22
|
};
|
|
22
23
|
export declare class AppWorkspaceActionsController {
|
|
23
24
|
private readonly host;
|
|
@@ -30,6 +31,7 @@ export declare class AppWorkspaceActionsController {
|
|
|
30
31
|
syncUserSessionEntryMetadata(): void;
|
|
31
32
|
copyUserMessage(entryId: string): Promise<void>;
|
|
32
33
|
forkFromUserMessage(entryId: string): Promise<void>;
|
|
34
|
+
forkFromUserMessageInNewTab(entryId: string): Promise<void>;
|
|
33
35
|
undoChangesFromUserMessage(entryId: string): Promise<void>;
|
|
34
36
|
private resolveUserSessionEntryId;
|
|
35
37
|
private getIdleRuntimeForAction;
|
|
@@ -108,6 +108,18 @@ export class AppWorkspaceActionsController {
|
|
|
108
108
|
this.host.setSessionStatus(runtime.session);
|
|
109
109
|
this.host.showToast("Session forked", "success");
|
|
110
110
|
}
|
|
111
|
+
async forkFromUserMessageInNewTab(entryId) {
|
|
112
|
+
const runtime = this.getIdleRuntimeForAction("fork in new tab");
|
|
113
|
+
if (!runtime)
|
|
114
|
+
return;
|
|
115
|
+
const entry = this.host.findUserEntry(entryId);
|
|
116
|
+
if (!entry)
|
|
117
|
+
throw new Error("User message is no longer available");
|
|
118
|
+
const sessionEntryId = this.resolveUserSessionEntryId(entry);
|
|
119
|
+
if (!sessionEntryId)
|
|
120
|
+
throw new Error("Session entry for this message is not available yet");
|
|
121
|
+
await this.host.forkSessionEntryInNewTab(sessionEntryId);
|
|
122
|
+
}
|
|
111
123
|
async undoChangesFromUserMessage(entryId) {
|
|
112
124
|
const runtime = this.getIdleRuntimeForAction("undo changes");
|
|
113
125
|
if (!runtime)
|
package/dist/config.d.ts
CHANGED
|
@@ -67,16 +67,20 @@ export type PixConfig = {
|
|
|
67
67
|
modelColors: ModelColorsConfig;
|
|
68
68
|
iconTheme: IconThemeConfig;
|
|
69
69
|
dictation: DictationConfig;
|
|
70
|
+
ignoreContextFiles: boolean;
|
|
70
71
|
};
|
|
71
72
|
export declare function getPixConfigPath(homeDir?: string): string;
|
|
73
|
+
export declare function getProjectPixConfigPath(cwd: string): string;
|
|
72
74
|
export declare function resolveDefaultModelRef(config: PixConfig): string | undefined;
|
|
73
75
|
export declare function savePixDefaultModel(modelRef: string): DefaultModelConfig | undefined;
|
|
74
76
|
export declare function savePixDefaultThinking(thinking: string, fallbackModelRef?: string): DefaultModelConfig | undefined;
|
|
75
77
|
export declare function savePixAutocompleteModel(modelRef: string): AutocompleteConfig;
|
|
78
|
+
export declare function saveProjectPixIgnoreContextFiles(cwd: string, ignoreContextFiles: boolean): boolean;
|
|
79
|
+
export declare function upsertPixIgnoreContextFilesInJsonc(source: string, ignoreContextFiles: boolean): string;
|
|
76
80
|
export declare function upsertPixDefaultModelInJsonc(source: string, modelRef: string): string;
|
|
77
81
|
export declare function upsertPixDefaultThinkingInJsonc(source: string, thinking: string, fallbackModelRef?: string): string;
|
|
78
82
|
export declare function upsertPixAutocompleteModelInJsonc(source: string, modelRef: string): string;
|
|
79
|
-
export declare function loadPixConfig(): PixConfig;
|
|
83
|
+
export declare function loadPixConfig(cwd?: string): PixConfig;
|
|
80
84
|
export declare function savePixDictationLanguage(language: string): void;
|
|
81
85
|
export declare function upsertPixDictationLanguageInJsonc(source: string, language: string): string;
|
|
82
86
|
export declare function resolveModelColor(modelRef: string, config: ModelColorsConfig): string | undefined;
|