pi-ui-extend 0.1.21 → 0.1.25

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.
Files changed (49) hide show
  1. package/README.md +1 -10
  2. package/bin/pix.mjs +11 -154
  3. package/dist/app/app.d.ts +1 -0
  4. package/dist/app/app.js +34 -9
  5. package/dist/app/cli/startup-info.d.ts +0 -1
  6. package/dist/app/cli/startup-info.js +0 -3
  7. package/dist/app/commands/command-session-actions.js +3 -0
  8. package/dist/app/input/autocomplete-controller.js +0 -1
  9. package/dist/app/popup/popup-menu-controller.js +7 -1
  10. package/dist/app/rendering/conversation-entry-renderer.js +29 -40
  11. package/dist/app/rendering/render-text.d.ts +6 -0
  12. package/dist/app/rendering/render-text.js +9 -0
  13. package/dist/app/rendering/tab-line-renderer.js +1 -5
  14. package/dist/app/rendering/tool-block-renderer.js +7 -1
  15. package/dist/app/screen/mouse-controller.js +14 -6
  16. package/dist/app/session/session-event-controller.js +5 -4
  17. package/dist/app/session/session-lifecycle-controller.js +0 -4
  18. package/dist/app/session/tabs-controller.d.ts +5 -1
  19. package/dist/app/session/tabs-controller.js +111 -23
  20. package/dist/app/types.d.ts +5 -0
  21. package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
  22. package/dist/app/workspace/workspace-actions-controller.js +71 -16
  23. package/dist/app/workspace/workspace-undo.js +41 -6
  24. package/dist/markdown-format.d.ts +4 -0
  25. package/dist/markdown-format.js +6 -1
  26. package/dist/schemas/pi-tools-suite-schema.d.ts +0 -1
  27. package/dist/schemas/pi-tools-suite-schema.js +0 -1
  28. package/dist/theme.js +18 -18
  29. package/extensions/session-title/config.ts +0 -5
  30. package/extensions/session-title/index.ts +0 -1
  31. package/external/pi-tools-suite/README.md +1 -1
  32. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  33. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -1
  34. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -5
  35. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +0 -1
  36. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +0 -1
  37. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +1 -1
  38. package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
  39. package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
  40. package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
  41. package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
  42. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
  43. package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
  44. package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
  45. package/external/pi-tools-suite/src/todo/index.ts +7 -6
  46. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
  47. package/external/pi-tools-suite/src/web-search/index.ts +139 -2
  48. package/package.json +7 -7
  49. package/schemas/pi-tools-suite.json +0 -6
@@ -79,7 +79,7 @@ export declare class AppTabsController {
79
79
  private runtimeForCommand;
80
80
  private idleRuntime;
81
81
  private activeTab;
82
- private settleStartupTabPlaceholders;
82
+ private clearStartupTabPlaceholders;
83
83
  private storeActiveRuntime;
84
84
  private setRuntimeForTab;
85
85
  private deleteRuntimeForTab;
@@ -109,6 +109,7 @@ export declare class AppTabsController {
109
109
  private sessionPath;
110
110
  private sessionTitle;
111
111
  private sessionTitleFromParts;
112
+ private updatedSessionTitle;
112
113
  private sessionActivity;
113
114
  private tabActivity;
114
115
  private clearTabAttention;
@@ -116,6 +117,9 @@ export declare class AppTabsController {
116
117
  private stopAttentionBlinkIfIdle;
117
118
  private restoredTabs;
118
119
  private defaultSessionTitleFromPath;
120
+ private loadSessionTitles;
121
+ private scheduleRestoredTabTitleRefresh;
122
+ private refreshRestoredTabTitles;
119
123
  private loadTabs;
120
124
  private parsePersistedInputState;
121
125
  private parsePersistedSubmittedUserMessages;
@@ -1,17 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { mkdir, open as openFile, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, join, resolve } from "node:path";
5
5
  import { getAgentDir, } from "@earendil-works/pi-coding-agent";
6
6
  import { isRecord } from "../guards.js";
7
7
  import { createId } from "../id.js";
8
- import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-info.js";
9
8
  import { tabPanelRows } from "../rendering/tab-line-renderer.js";
10
9
  const TAB_STATE_VERSION = 3;
11
10
  const MAX_RESTORED_TABS = 8;
12
11
  const BACKGROUND_PREWARM_TAB_LIMIT = 2;
13
12
  const TAB_ATTENTION_BLINK_KEY = "tab-attention";
14
13
  const LOADING_TAB_TITLE_PATTERN = /^loading(?:…|\.\.\.)?$/iu;
14
+ const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
15
+ const SESSION_TITLE_SCAN_MAX_BYTES = 2 * 1024 * 1024;
15
16
  export class AppTabsController {
16
17
  host;
17
18
  tabItems = [];
@@ -140,7 +141,7 @@ export class AppTabsController {
140
141
  return;
141
142
  }
142
143
  if (!active) {
143
- const tab = this.tabFromSession(session, { titlePlaceholder: this.restored ? "new" : "loading" });
144
+ const tab = this.tabFromSession(session, { titlePlaceholder: "loading" });
144
145
  this.tabItems.push(tab);
145
146
  this.activeTabId = tab.id;
146
147
  this.clearTabAttention(tab);
@@ -164,18 +165,18 @@ export class AppTabsController {
164
165
  return;
165
166
  this.syncActiveTabFromRuntime({ save: false });
166
167
  if (this.host.options.noSession) {
167
- this.settleStartupTabPlaceholders();
168
+ this.clearStartupTabPlaceholders();
168
169
  return;
169
170
  }
170
171
  const saved = await this.loadTabs();
171
172
  if (!saved || saved.tabs.length === 0) {
172
- this.settleStartupTabPlaceholders();
173
+ this.clearStartupTabPlaceholders();
173
174
  await this.saveTabs();
174
175
  return;
175
176
  }
176
177
  const restoredTabs = this.restoredTabs(saved);
177
178
  if (restoredTabs.length === 0) {
178
- this.settleStartupTabPlaceholders();
179
+ this.clearStartupTabPlaceholders();
179
180
  await this.saveTabs();
180
181
  this.scheduleProjectSessionRetention();
181
182
  return;
@@ -191,11 +192,13 @@ export class AppTabsController {
191
192
  this.replaceTabs(restoredTabs, desiredPath);
192
193
  this.restorePersistedInputStates(saved);
193
194
  this.restorePersistedDeferredUserMessages(saved);
195
+ const restoredSessionPaths = saved.tabs.map((tab) => tab.path);
194
196
  if (explicitSessionPath && currentPath)
195
197
  this.ensureCurrentSessionTab(runtime.session);
196
198
  if (!desiredPath) {
197
- this.settleStartupTabPlaceholders();
199
+ this.clearStartupTabPlaceholders();
198
200
  await this.saveTabs();
201
+ this.scheduleRestoredTabTitleRefresh(restoredSessionPaths);
199
202
  this.scheduleProjectSessionRetention();
200
203
  this.scheduleTabPrewarm();
201
204
  return;
@@ -212,20 +215,22 @@ export class AppTabsController {
212
215
  this.host.showToast("Could not restore the previous active tab", "warning");
213
216
  this.replaceTabs([this.tabFromSession(runtime.session), ...restoredTabs], currentPath);
214
217
  this.storeActiveRuntime(runtime);
215
- this.settleStartupTabPlaceholders();
218
+ this.clearStartupTabPlaceholders();
216
219
  await this.saveTabs();
220
+ this.scheduleRestoredTabTitleRefresh(restoredSessionPaths);
217
221
  this.scheduleProjectSessionRetention();
218
222
  return;
219
223
  }
220
224
  }
221
225
  this.syncActiveTabFromRuntime({ save: false });
222
- this.settleStartupTabPlaceholders();
226
+ this.clearStartupTabPlaceholders();
223
227
  if (this.activeTabId)
224
228
  this.restoreInputState(this.activeTabId);
225
229
  await this.saveTabs();
226
230
  this.scheduleProjectSessionRetention();
227
231
  this.scheduleTabPrewarm();
228
232
  await this.loadActiveSessionHistory(restoredRuntime);
233
+ this.scheduleRestoredTabTitleRefresh(restoredSessionPaths);
229
234
  }
230
235
  async openNewTab() {
231
236
  if (this.pendingActiveTabId) {
@@ -307,12 +312,7 @@ export class AppTabsController {
307
312
  this.scheduleProjectSessionRetention();
308
313
  this.host.resetSessionView();
309
314
  this.restoreDeferredUserMessages(targetTab.id);
310
- if (isEmptyStartupSession(newRuntime)) {
311
- this.host.addEntry({ id: createId("system"), kind: "system", text: createStartupInfoMessage(newRuntime) });
312
- }
313
- else {
314
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Opened a new tab. cwd=${newRuntime.cwd}` });
315
- }
315
+ this.host.addEntry({ id: createId("system"), kind: "system", text: `Opened a new tab. cwd=${newRuntime.cwd}` });
316
316
  if (newRuntime.modelFallbackMessage)
317
317
  this.host.addEntry({ id: createId("system"), kind: "system", text: newRuntime.modelFallbackMessage });
318
318
  for (const diag of newRuntime.diagnostics ?? []) {
@@ -796,10 +796,9 @@ export class AppTabsController {
796
796
  activeTab() {
797
797
  return this.activeTabId ? this.tabItems.find((tab) => tab.id === this.activeTabId) : undefined;
798
798
  }
799
- settleStartupTabPlaceholders() {
799
+ clearStartupTabPlaceholders() {
800
800
  for (const tab of this.tabItems) {
801
- if (tab.titlePlaceholder === "loading")
802
- tab.titlePlaceholder = "new";
801
+ delete tab.titlePlaceholder;
803
802
  }
804
803
  }
805
804
  storeActiveRuntime(runtime = this.host.runtime()) {
@@ -1071,9 +1070,10 @@ export class AppTabsController {
1071
1070
  };
1072
1071
  }
1073
1072
  updateTabFromSession(tab, session) {
1074
- tab.title = this.sessionTitle(session);
1075
- tab.activity = this.sessionActivity(session);
1073
+ const previousSessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
1076
1074
  const sessionPath = this.sessionPath(session);
1075
+ tab.title = this.updatedSessionTitle(tab.title, this.sessionTitle(session), previousSessionPath, sessionPath, tab.titlePlaceholder !== undefined);
1076
+ tab.activity = this.sessionActivity(session);
1077
1077
  if (sessionPath)
1078
1078
  tab.sessionPath = sessionPath;
1079
1079
  }
@@ -1087,6 +1087,15 @@ export class AppTabsController {
1087
1087
  const name = sessionName?.trim();
1088
1088
  return name && !LOADING_TAB_TITLE_PATTERN.test(name) ? name : `session ${sessionId.slice(0, 8)}`;
1089
1089
  }
1090
+ updatedSessionTitle(currentTitle, nextTitle, currentSessionPath, nextSessionPath, hasTitlePlaceholder) {
1091
+ if (!isDefaultSessionTitle(nextTitle))
1092
+ return nextTitle;
1093
+ if (hasTitlePlaceholder)
1094
+ return nextTitle;
1095
+ if (currentSessionPath !== undefined && nextSessionPath !== undefined && currentSessionPath !== nextSessionPath)
1096
+ return nextTitle;
1097
+ return validSessionTitle(currentTitle) && !isDefaultSessionTitle(currentTitle) ? currentTitle : nextTitle;
1098
+ }
1090
1099
  sessionActivity(session) {
1091
1100
  return session?.isStreaming || session?.isCompacting ? "running" : "idle";
1092
1101
  }
@@ -1117,7 +1126,7 @@ export class AppTabsController {
1117
1126
  initialVisible: true,
1118
1127
  });
1119
1128
  }
1120
- restoredTabs(saved) {
1129
+ restoredTabs(saved, sessionTitles = new Map()) {
1121
1130
  const tabs = [];
1122
1131
  const seen = new Set();
1123
1132
  for (const tab of saved.tabs) {
@@ -1129,11 +1138,11 @@ export class AppTabsController {
1129
1138
  seen.add(sessionPath);
1130
1139
  const savedTitle = tab.title?.trim();
1131
1140
  const restoredLoadingTitle = savedTitle !== undefined && LOADING_TAB_TITLE_PATTERN.test(savedTitle);
1132
- const title = restoredLoadingTitle ? this.defaultSessionTitleFromPath(sessionPath) : savedTitle;
1141
+ const sessionTitle = validSessionTitle(sessionTitles.get(sessionPath));
1142
+ const title = sessionTitle || (restoredLoadingTitle ? this.defaultSessionTitleFromPath(sessionPath) : savedTitle);
1133
1143
  tabs.push({
1134
1144
  id: createId("tab"),
1135
1145
  title: title || "session",
1136
- ...(restoredLoadingTitle ? { titlePlaceholder: "new" } : {}),
1137
1146
  status: "waiting",
1138
1147
  sessionPath,
1139
1148
  });
@@ -1148,6 +1157,40 @@ export class AppTabsController {
1148
1157
  ?? createHash("sha256").update(sessionPath).digest("hex").slice(0, 8);
1149
1158
  return `session ${sessionId}`;
1150
1159
  }
1160
+ async loadSessionTitles(sessionPaths) {
1161
+ const uniquePaths = [...new Set(sessionPaths.map((sessionPath) => resolve(sessionPath)))].slice(0, MAX_RESTORED_TABS);
1162
+ const entries = await Promise.all(uniquePaths.map(async (sessionPath) => {
1163
+ const title = await readLatestSessionTitle(sessionPath);
1164
+ return title ? [sessionPath, title] : undefined;
1165
+ }));
1166
+ return new Map(entries.filter((entry) => entry !== undefined));
1167
+ }
1168
+ scheduleRestoredTabTitleRefresh(sessionPaths) {
1169
+ if (sessionPaths.length === 0)
1170
+ return;
1171
+ setTimeout(() => {
1172
+ void this.refreshRestoredTabTitles(sessionPaths);
1173
+ }, 0).unref?.();
1174
+ }
1175
+ async refreshRestoredTabTitles(sessionPaths) {
1176
+ const titles = await this.loadSessionTitles(sessionPaths);
1177
+ if (titles.size === 0)
1178
+ return;
1179
+ let changed = false;
1180
+ for (const tab of this.tabItems) {
1181
+ const sessionPath = tab.sessionPath ? resolve(tab.sessionPath) : undefined;
1182
+ const title = sessionPath ? titles.get(sessionPath) : undefined;
1183
+ if (!title || tab.title === title)
1184
+ continue;
1185
+ tab.title = title;
1186
+ delete tab.titlePlaceholder;
1187
+ changed = true;
1188
+ }
1189
+ if (!changed)
1190
+ return;
1191
+ void this.saveTabs();
1192
+ this.host.render();
1193
+ }
1151
1194
  async loadTabs() {
1152
1195
  try {
1153
1196
  const raw = await readFile(this.filePath(), "utf8");
@@ -1408,6 +1451,51 @@ function parsePersistedImage(value) {
1408
1451
  ? { type: "image", data: value.data, mimeType: value.mimeType }
1409
1452
  : undefined;
1410
1453
  }
1454
+ async function readLatestSessionTitle(sessionPath) {
1455
+ let file;
1456
+ try {
1457
+ file = await openFile(sessionPath, "r");
1458
+ const { size } = await file.stat();
1459
+ if (size <= 0)
1460
+ return undefined;
1461
+ const byteCount = Math.min(size, SESSION_TITLE_SCAN_MAX_BYTES);
1462
+ const buffer = Buffer.alloc(byteCount);
1463
+ await file.read(buffer, 0, byteCount, size - byteCount);
1464
+ const text = buffer.toString("utf8");
1465
+ const lines = text.split("\n");
1466
+ if (size > byteCount)
1467
+ lines.shift();
1468
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1469
+ const line = lines[index]?.trim();
1470
+ if (!line)
1471
+ continue;
1472
+ let parsed;
1473
+ try {
1474
+ parsed = JSON.parse(line);
1475
+ }
1476
+ catch {
1477
+ continue;
1478
+ }
1479
+ if (!isRecord(parsed) || parsed.type !== "session_info" || typeof parsed.name !== "string")
1480
+ continue;
1481
+ return validSessionTitle(parsed.name);
1482
+ }
1483
+ }
1484
+ catch {
1485
+ return undefined;
1486
+ }
1487
+ finally {
1488
+ await file?.close();
1489
+ }
1490
+ return undefined;
1491
+ }
1492
+ function validSessionTitle(value) {
1493
+ const title = value?.trim();
1494
+ return title && !LOADING_TAB_TITLE_PATTERN.test(title) ? title : undefined;
1495
+ }
1496
+ function isDefaultSessionTitle(value) {
1497
+ return DEFAULT_SESSION_TITLE_PATTERN.test(value.trim());
1498
+ }
1411
1499
  function clonePersistedAttachment(attachment) {
1412
1500
  if (attachment.kind === "image")
1413
1501
  return { kind: "image", tag: attachment.tag, image: { ...attachment.image } };
@@ -201,6 +201,8 @@ export type SubagentsWidgetState = {
201
201
  };
202
202
  export type RenderedLine = {
203
203
  text: string;
204
+ copyText?: string;
205
+ continuesOnNextLine?: boolean;
204
206
  variant?: "normal" | "muted" | "error" | "accent";
205
207
  colorOverride?: string;
206
208
  backgroundOverride?: string;
@@ -512,6 +514,9 @@ export type UserMessageMenuValue = "copy" | "fork" | "fork-new-tab" | "undo";
512
514
  export type UserMessageJumpMenuValue = {
513
515
  entryId?: string;
514
516
  sessionEntryId?: string;
517
+ text?: string;
518
+ userIndex?: number;
519
+ userCount?: number;
515
520
  };
516
521
  export type QueueMessageMenuValue = "cancel" | "edit" | "send-now";
517
522
  export type ResumeMenuValue = {
@@ -33,6 +33,9 @@ export declare class AppWorkspaceActionsController {
33
33
  forkFromUserMessage(entryId: string): Promise<void>;
34
34
  forkFromUserMessageInNewTab(entryId: string): Promise<void>;
35
35
  undoChangesFromUserMessage(entryId: string): Promise<void>;
36
+ private workspaceMutationPlanFromSessionEntry;
37
+ private workspaceMutationPlanForSingleEntry;
38
+ private findUserEntryBySessionEntryId;
36
39
  private resolveUserSessionEntryId;
37
40
  private getIdleRuntimeForAction;
38
41
  private workspaceMutationsForSessionEntry;
@@ -130,18 +130,7 @@ export class AppWorkspaceActionsController {
130
130
  const sessionEntryId = this.resolveUserSessionEntryId(entry);
131
131
  if (!sessionEntryId)
132
132
  throw new Error("Session entry for this message is not available yet");
133
- const hasMutationLog = entry.workspaceMutations !== undefined || this.hasWorkspaceMutationsForSessionEntry(sessionEntryId);
134
- const mutations = entry.workspaceMutations ?? this.workspaceMutationsForSessionEntry(sessionEntryId);
135
- if (!hasMutationLog) {
136
- throw new Error("No workspace mutation log was captured for this message. Undo is available for messages sent after this build.");
137
- }
138
- if (mutations.length > 0) {
139
- this.host.setStatus("reverting recorded commands");
140
- this.host.render();
141
- }
142
- const reverted = mutations.length === 0 ? { ok: true, changedFiles: 0, revertedChanges: 0 } : await revertWorkspaceMutations(runtime.cwd, mutations);
143
- if (!reverted.ok)
144
- throw new Error(reverted.error);
133
+ const mutationPlan = this.workspaceMutationPlanFromSessionEntry(sessionEntryId);
145
134
  this.host.setStatus("truncating session");
146
135
  this.host.render();
147
136
  const result = await runtime.session.navigateTree(sessionEntryId);
@@ -157,15 +146,69 @@ export class AppWorkspaceActionsController {
157
146
  }
158
147
  this.host.resetSessionView();
159
148
  this.host.loadSessionHistory();
160
- if (result.editorText && !this.host.getInput().trim())
161
- this.host.setInput(result.editorText);
149
+ this.host.setInput(result.editorText ?? entry.text);
150
+ let revertSummary = "No recorded file mutations were found for the removed branch.";
151
+ let revertToastKind = "success";
152
+ if (mutationPlan.mutations.length > 0) {
153
+ this.host.setStatus("reverting recorded commands");
154
+ this.host.render();
155
+ const reverted = await revertWorkspaceMutations(runtime.cwd, mutationPlan.mutations);
156
+ if (reverted.ok) {
157
+ revertSummary = `Reverted ${reverted.revertedChanges} command${reverted.revertedChanges === 1 ? "" : "s"} across ${reverted.changedFiles} file${reverted.changedFiles === 1 ? "" : "s"}.`;
158
+ }
159
+ else {
160
+ revertSummary = `Workspace revert failed: ${reverted.error}`;
161
+ revertToastKind = "warning";
162
+ }
163
+ }
164
+ else if (mutationPlan.messagesWithoutLogs > 0) {
165
+ revertSummary = `No recorded file mutations were available for ${mutationPlan.messagesWithoutLogs} removed message${mutationPlan.messagesWithoutLogs === 1 ? "" : "s"}.`;
166
+ revertToastKind = "warning";
167
+ }
162
168
  this.host.addEntry({
163
169
  id: createId("system"),
164
170
  kind: "system",
165
- text: `Undid changes from entry ${sessionEntryId}. Reverted ${reverted.revertedChanges} command${reverted.revertedChanges === 1 ? "" : "s"} across ${reverted.changedFiles} file${reverted.changedFiles === 1 ? "" : "s"}.`,
171
+ text: `Undid changes from entry ${sessionEntryId}. ${revertSummary}`,
166
172
  });
167
173
  this.host.setSessionStatus(runtime.session);
168
- this.host.showToast("Changes undone", "success");
174
+ this.host.showToast(revertToastKind === "success" ? "Changes undone" : "Session rewound with revert warnings", revertToastKind);
175
+ }
176
+ workspaceMutationPlanFromSessionEntry(entryId) {
177
+ const runtime = this.host.runtime();
178
+ if (!runtime)
179
+ return { mutations: [], messagesWithoutLogs: 0 };
180
+ const branch = runtime.session.sessionManager.getBranch();
181
+ const startIndex = branch.findIndex((entry) => entry.id === entryId);
182
+ if (startIndex < 0) {
183
+ return this.workspaceMutationPlanForSingleEntry(entryId);
184
+ }
185
+ const mutations = [];
186
+ let messagesWithoutLogs = 0;
187
+ for (const branchEntry of branch.slice(startIndex)) {
188
+ if (branchEntry.type !== "message")
189
+ continue;
190
+ if (!isRecord(branchEntry.message) || branchEntry.message.role !== "user")
191
+ continue;
192
+ const visibleEntry = this.findUserEntryBySessionEntryId(branchEntry.id);
193
+ const hasMutationLog = visibleEntry?.workspaceMutations !== undefined || this.hasWorkspaceMutationsForSessionEntry(branchEntry.id);
194
+ const entryMutations = visibleEntry?.workspaceMutations ?? this.workspaceMutationsForSessionEntry(branchEntry.id);
195
+ if (!hasMutationLog) {
196
+ messagesWithoutLogs += 1;
197
+ continue;
198
+ }
199
+ mutations.push(...entryMutations);
200
+ }
201
+ return { mutations, messagesWithoutLogs };
202
+ }
203
+ workspaceMutationPlanForSingleEntry(entryId) {
204
+ const visibleEntry = this.findUserEntryBySessionEntryId(entryId);
205
+ const hasMutationLog = visibleEntry?.workspaceMutations !== undefined || this.hasWorkspaceMutationsForSessionEntry(entryId);
206
+ if (!hasMutationLog)
207
+ return { mutations: [], messagesWithoutLogs: 1 };
208
+ return { mutations: visibleEntry?.workspaceMutations ?? this.workspaceMutationsForSessionEntry(entryId), messagesWithoutLogs: 0 };
209
+ }
210
+ findUserEntryBySessionEntryId(sessionEntryId) {
211
+ return this.host.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
169
212
  }
170
213
  resolveUserSessionEntryId(entry) {
171
214
  if (!entry.sessionEntryId)
@@ -204,6 +247,18 @@ export class AppWorkspaceActionsController {
204
247
  const key = this.workspaceUndoIndexKey(entryId);
205
248
  if (!key)
206
249
  return;
250
+ if (mutations.length === 0) {
251
+ if (!Object.prototype.hasOwnProperty.call(this.workspaceUndoIndex.entries, key))
252
+ return;
253
+ delete this.workspaceUndoIndex.entries[key];
254
+ try {
255
+ saveWorkspaceUndoIndex(getAgentDir(), this.workspaceUndoIndex);
256
+ }
257
+ catch {
258
+ // Undo persistence is best-effort; in-memory undo still works for this run.
259
+ }
260
+ return;
261
+ }
207
262
  const hasExisting = Object.prototype.hasOwnProperty.call(this.workspaceUndoIndex.entries, key);
208
263
  if (hasExisting && sameWorkspaceMutations(this.workspaceUndoIndex.entries[key] ?? [], mutations))
209
264
  return;
@@ -25,7 +25,7 @@ export function prepareWorkspaceMutation(cwd, toolName, args) {
25
25
  if (name !== "write")
26
26
  return undefined;
27
27
  const record = plainRecord(args);
28
- const rawPath = stringValue(record?.path);
28
+ const rawPath = toolPathValue(record);
29
29
  const afterContent = stringValue(record?.content);
30
30
  if (!rawPath || afterContent === undefined)
31
31
  return undefined;
@@ -45,8 +45,9 @@ export function workspaceMutationFromToolExecution(input) {
45
45
  const name = normalizedToolName(input.toolName);
46
46
  if (name === "write" && input.preparation?.type === "write") {
47
47
  const record = plainRecord(input.args);
48
+ const rawPath = toolPathValue(record);
48
49
  const afterContent = stringValue(record?.content);
49
- if (afterContent === undefined || input.preparation.beforeContent === afterContent)
50
+ if (!rawPath || afterContent === undefined || input.preparation.beforeContent === afterContent)
50
51
  return undefined;
51
52
  return {
52
53
  type: "write",
@@ -56,7 +57,8 @@ export function workspaceMutationFromToolExecution(input) {
56
57
  toolName: input.toolName,
57
58
  };
58
59
  }
59
- const patch = patchFromDetails(input.details) ?? patchFromArgs(input.args);
60
+ const rawPatch = patchFromDetails(input.details) ?? patchFromArgs(input.args);
61
+ const patch = rawPatch && normalizePatchForWorkspace(input.cwd, rawPatch);
60
62
  if (!patch || !looksLikeUnifiedPatch(patch))
61
63
  return undefined;
62
64
  if (name === "edit" || name === "apply_patch" || name === "ast_apply") {
@@ -94,14 +96,17 @@ async function applyMutation(cwd, mutation, direction) {
94
96
  return applyWriteMutation(cwd, mutation, direction);
95
97
  }
96
98
  async function applyPatchMutation(cwd, mutation, direction) {
99
+ const patch = normalizePatchForWorkspace(cwd, mutation.patch);
100
+ if (!patch)
101
+ return { ok: false, error: "Refusing to apply patch with paths outside workspace." };
97
102
  const args = ["apply", ...(direction === "undo" ? ["--reverse"] : []), "--whitespace=nowarn"];
98
- const check = await runGitApply(cwd, [...args, "--check"], mutation.patch);
103
+ const check = await runGitApply(cwd, [...args, "--check"], patch);
99
104
  if (check.status !== 0)
100
105
  return { ok: false, error: commandError(`git ${args.join(" ")} --check`, check) };
101
- const apply = await runGitApply(cwd, args, mutation.patch);
106
+ const apply = await runGitApply(cwd, args, patch);
102
107
  if (apply.status !== 0)
103
108
  return { ok: false, error: commandError(`git ${args.join(" ")}`, apply) };
104
- return { ok: true, changedFiles: filesFromPatch(mutation.patch) };
109
+ return { ok: true, changedFiles: filesFromPatch(patch) };
105
110
  }
106
111
  async function applyWriteMutation(cwd, mutation, direction) {
107
112
  const safePath = safeRelativePath(cwd, mutation.path);
@@ -163,6 +168,9 @@ function plainRecord(value) {
163
168
  function stringValue(value) {
164
169
  return typeof value === "string" ? value : undefined;
165
170
  }
171
+ function toolPathValue(record) {
172
+ return stringValue(record?.path) ?? stringValue(record?.file_path);
173
+ }
166
174
  function patchFromDetails(details) {
167
175
  const record = plainRecord(details);
168
176
  return stringValue(record?.patch) ?? stringValue(record?.diff);
@@ -174,6 +182,33 @@ function patchFromArgs(args) {
174
182
  function looksLikeUnifiedPatch(text) {
175
183
  return /^---\s+/m.test(text) && /^\+\+\+\s+/m.test(text) && /^@@\s/m.test(text);
176
184
  }
185
+ function normalizePatchForWorkspace(cwd, patch) {
186
+ const normalizedLines = [];
187
+ for (const line of patch.split("\n")) {
188
+ const match = /^(---|\+\+\+)\s+(\S+)(.*)$/.exec(line);
189
+ if (!match) {
190
+ normalizedLines.push(line);
191
+ continue;
192
+ }
193
+ const marker = match[1];
194
+ const rawPath = match[2];
195
+ const suffix = match[3] ?? "";
196
+ if (rawPath === "/dev/null") {
197
+ normalizedLines.push(line);
198
+ continue;
199
+ }
200
+ const relativePath = patchWorkspacePath(cwd, rawPath);
201
+ if (!relativePath)
202
+ return undefined;
203
+ const prefix = marker === "---" ? "a" : "b";
204
+ normalizedLines.push(`${marker} ${prefix}/${relativePath}${suffix}`);
205
+ }
206
+ return normalizedLines.join("\n");
207
+ }
208
+ function patchWorkspacePath(cwd, inputPath) {
209
+ const path = !isAbsolute(inputPath) && /^[ab]\//u.test(inputPath) ? inputPath.slice(2) : inputPath;
210
+ return safeRelativePath(cwd, path);
211
+ }
177
212
  function filesFromPatch(patch) {
178
213
  const files = new Set();
179
214
  for (const line of patch.split("\n")) {
@@ -1,6 +1,8 @@
1
1
  import { type SyntaxLineHighlight, type ToolBodySyntaxHighlights } from "./syntax-highlight.js";
2
2
  export type RenderedMarkdownLine = {
3
3
  text: string;
4
+ copyText?: string;
5
+ continuesOnNextLine?: boolean;
4
6
  segments: readonly {
5
7
  start: number;
6
8
  end: number;
@@ -10,6 +12,8 @@ export type RenderedMarkdownLine = {
10
12
  };
11
13
  export type RenderedMarkdownTextLine = {
12
14
  text: string;
15
+ copyText?: string;
16
+ continuesOnNextLine?: boolean;
13
17
  segments?: readonly {
14
18
  start: number;
15
19
  end: number;
@@ -86,6 +86,8 @@ export function renderMarkdownTextLines(text, width, start = 0) {
86
86
  for (const wrapped of wrapRenderedMarkdownLine(markdownLine ?? { text: rawLine, segments: [] }, width)) {
87
87
  lines.push({
88
88
  text: wrapped.text,
89
+ ...(wrapped.copyText === undefined ? {} : { copyText: wrapped.copyText }),
90
+ ...(wrapped.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
89
91
  ...(wrapped.segments.length > 0 ? { segments: wrapped.segments } : {}),
90
92
  ...(syntaxHighlight ? { syntaxHighlight } : {}),
91
93
  ...(isHeadingLine ? { heading: true } : {}),
@@ -124,8 +126,11 @@ function wrapRenderedMarkdownLine(line, width) {
124
126
  const safeWidth = Math.max(1, width);
125
127
  if (stringDisplayWidth(line.text) <= safeWidth)
126
128
  return [line];
127
- return wrapDisplayLineByWordsWithRanges(line.text, safeWidth).map((range) => ({
129
+ const ranges = wrapDisplayLineByWordsWithRanges(line.text, safeWidth);
130
+ return ranges.map((range, index) => ({
128
131
  text: range.text,
132
+ copyText: line.text.slice(range.start, ranges[index + 1]?.start ?? range.end),
133
+ ...(index < ranges.length - 1 ? { continuesOnNextLine: true } : {}),
129
134
  segments: line.segments.flatMap((segment) => shiftSegmentToRange(segment, range.start, range.end)),
130
135
  }));
131
136
  }
@@ -83,7 +83,6 @@ export declare const PiToolsSuiteConfigSchema: Type.TObject<{
83
83
  maxTaskChars: Type.TOptional<Type.TNumber>;
84
84
  maxTokens: Type.TOptional<Type.TNumber>;
85
85
  maxRetries: Type.TOptional<Type.TNumber>;
86
- temperature: Type.TOptional<Type.TNumber>;
87
86
  timeoutMs: Type.TOptional<Type.TNumber>;
88
87
  debug: Type.TOptional<Type.TBoolean>;
89
88
  }>>;
@@ -115,7 +115,6 @@ const SubagentRoutingConfig = Type.Object({
115
115
  maxTaskChars: Type.Optional(Type.Number({ description: "Max task/scope characters sent to router.", minimum: 100 })),
116
116
  maxTokens: Type.Optional(Type.Number({ description: "Max router response tokens.", minimum: 8 })),
117
117
  maxRetries: Type.Optional(Type.Number({ description: "Router request retries.", minimum: 0 })),
118
- temperature: Type.Optional(Type.Number({ description: "Router sampling temperature.", minimum: 0, maximum: 2 })),
119
118
  timeoutMs: Type.Optional(Type.Number({ description: "Router request timeout in ms.", minimum: 1000 })),
120
119
  debug: Type.Optional(Type.Boolean({ description: "Show routing debug warnings." })),
121
120
  }, { description: "LLM-based role routing configuration." });
package/dist/theme.js CHANGED
@@ -36,15 +36,15 @@ export const THEMES = {
36
36
  warning: "#d49a4a",
37
37
  heading: "#d4b35e",
38
38
  info: "#7fb3c8",
39
- toolMutation: "#b8899e", // ~10% brighter from #a67c8f
40
- toolSearch: "#9d8abb", // ~10% brighter from #8c7aa8
41
- toolTitle: "#899199", // ~10% brighter from #7b848c
42
- toolBash: "#b89071", // ~10% brighter from #a68266
43
- toolRead: "#6d9b82", // ~10% brighter from #628c76
44
- toolIndex: "#7692b4", // ~10% brighter from #6a84a3
45
- toolEdit: "#b47389", // ~10% brighter from #a3687c
46
- toolWeb: "#8192b6", // ~10% brighter from #7584a6
47
- toolMeta: "#7d8192", // ~10% brighter from #707485
39
+ toolMutation: "#b87f98",
40
+ toolSearch: "#9780bb",
41
+ toolTitle: "#858f99",
42
+ toolBash: "#b88862",
43
+ toolRead: "#639b7c",
44
+ toolIndex: "#698bb4",
45
+ toolEdit: "#b46680",
46
+ toolWeb: "#768ab6",
47
+ toolMeta: "#787d92",
48
48
  thinkingForeground: "#64748b",
49
49
  userForeground: "#d97706",
50
50
  thinkingXHigh: "#ff8a86",
@@ -91,15 +91,15 @@ export const THEMES = {
91
91
  warning: "#9a631d",
92
92
  heading: "#b88a28",
93
93
  info: "#246b8e",
94
- toolMutation: "#8c5c70", // reverted
95
- toolSearch: "#6e608c", // reverted
96
- toolTitle: "#626c78", // reverted
97
- toolBash: "#8c7556", // reverted
98
- toolRead: "#507a62", // reverted
99
- toolIndex: "#567a96", // reverted
100
- toolEdit: "#8c586c", // reverted
101
- toolWeb: "#5c6a8c", // reverted
102
- toolMeta: "#787e8a", // reverted
94
+ toolMutation: "#8c526a",
95
+ toolSearch: "#68578c",
96
+ toolTitle: "#5d6978",
97
+ toolBash: "#8c704b",
98
+ toolRead: "#477a5d",
99
+ toolIndex: "#497496",
100
+ toolEdit: "#8c4d65",
101
+ toolWeb: "#52638c",
102
+ toolMeta: "#747b8a",
103
103
  thinkingForeground: "#6b5491",
104
104
  userForeground: "#854d0e",
105
105
  thinkingXHigh: "#cf333d",
@@ -12,7 +12,6 @@ export interface SessionTitleConfig {
12
12
  maxRetries: number;
13
13
  generationAttempts: number;
14
14
  retryDelayMs: number;
15
- temperature: number;
16
15
  timeoutMs: number;
17
16
  terminalTitle: boolean;
18
17
  terminalTitlePrefix: string;
@@ -29,7 +28,6 @@ const DEFAULT_CONFIG: SessionTitleConfig = {
29
28
  maxRetries: 2,
30
29
  generationAttempts: 3,
31
30
  retryDelayMs: 3000,
32
- temperature: 0.2,
33
31
  timeoutMs: 12_000,
34
32
  terminalTitle: true,
35
33
  terminalTitlePrefix: "pi — ",
@@ -83,9 +81,6 @@ function mergeConfig(base: SessionTitleConfig, raw: Record<string, unknown>): Se
83
81
  if (typeof raw.retryDelayMs === "number" && Number.isFinite(raw.retryDelayMs)) {
84
82
  next.retryDelayMs = Math.max(250, Math.floor(raw.retryDelayMs));
85
83
  }
86
- if (typeof raw.temperature === "number" && Number.isFinite(raw.temperature)) {
87
- next.temperature = Math.min(2, Math.max(0, raw.temperature));
88
- }
89
84
  if (typeof raw.timeoutMs === "number" && Number.isFinite(raw.timeoutMs)) {
90
85
  next.timeoutMs = Math.max(1000, Math.floor(raw.timeoutMs));
91
86
  }