pi-ui-extend 0.1.20 → 0.1.24

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 (54) hide show
  1. package/README.md +1 -10
  2. package/bin/pix.mjs +11 -154
  3. package/dist/app/app.d.ts +4 -0
  4. package/dist/app/app.js +102 -17
  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/extensions/extension-ui-controller.js +2 -2
  9. package/dist/app/input/voice-controller.d.ts +3 -2
  10. package/dist/app/input/voice-controller.js +9 -0
  11. package/dist/app/popup/popup-menu-controller.js +7 -1
  12. package/dist/app/rendering/conversation-entry-renderer.js +29 -10
  13. package/dist/app/rendering/conversation-tool-renderer.js +1 -1
  14. package/dist/app/rendering/conversation-viewport.d.ts +1 -5
  15. package/dist/app/rendering/conversation-viewport.js +9 -16
  16. package/dist/app/rendering/editor-layout-renderer.js +1 -1
  17. package/dist/app/rendering/render-text.d.ts +6 -0
  18. package/dist/app/rendering/render-text.js +9 -0
  19. package/dist/app/rendering/tab-line-renderer.js +1 -5
  20. package/dist/app/rendering/tool-block-renderer.d.ts +2 -0
  21. package/dist/app/rendering/tool-block-renderer.js +20 -2
  22. package/dist/app/runtime.d.ts +2 -0
  23. package/dist/app/runtime.js +27 -4
  24. package/dist/app/screen/mouse-controller.js +14 -6
  25. package/dist/app/screen/screen-styler.js +2 -2
  26. package/dist/app/session/session-event-controller.js +5 -4
  27. package/dist/app/session/session-lifecycle-controller.d.ts +2 -0
  28. package/dist/app/session/session-lifecycle-controller.js +43 -20
  29. package/dist/app/session/tabs-controller.d.ts +6 -2
  30. package/dist/app/session/tabs-controller.js +114 -30
  31. package/dist/app/types.d.ts +5 -0
  32. package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
  33. package/dist/app/workspace/workspace-actions-controller.js +71 -16
  34. package/dist/app/workspace/workspace-undo.js +41 -6
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.js +19 -7
  37. package/dist/markdown-format.d.ts +6 -0
  38. package/dist/markdown-format.js +11 -3
  39. package/dist/syntax-highlight.js +3 -1
  40. package/dist/theme.d.ts +3 -0
  41. package/dist/theme.js +53 -28
  42. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +6 -1
  43. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +2 -1
  44. package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
  45. package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
  46. package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
  47. package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
  48. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
  49. package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
  50. package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
  51. package/external/pi-tools-suite/src/todo/index.ts +7 -6
  52. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
  53. package/external/pi-tools-suite/src/web-search/index.ts +139 -2
  54. package/package.json +7 -7
@@ -1,12 +1,9 @@
1
- import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
1
  import { type PixConfig } from "../../config.js";
3
2
  import type { Theme } from "../../theme.js";
4
3
  import { type InlineUserMessageMenuContext } from "./conversation-entry-renderer.js";
5
- import type { ConversationBlockCache, Entry, RenderedLine, SubmittedUserMessage } from "../types.js";
4
+ import type { ConversationBlockCache, Entry, RenderedLine } from "../types.js";
6
5
  export type ConversationViewportHost = {
7
6
  readonly entries: readonly Entry[];
8
- readonly session: AgentSession | undefined;
9
- readonly deferredUserMessages: readonly SubmittedUserMessage[];
10
7
  readonly entryRenderVersions: ReadonlyMap<string, number>;
11
8
  readonly cwd: string;
12
9
  readonly colors: Theme["colors"];
@@ -40,7 +37,6 @@ export declare class ConversationViewport {
40
37
  blockForEntry(entry: Entry, width: number): ConversationBlockCache;
41
38
  entryBlockPositions(width: number): ConversationEntryBlockPosition[];
42
39
  measuredLineCountForEntries(width: number, entryIds: readonly string[]): number;
43
- private queuedEntries;
44
40
  private layoutForWidth;
45
41
  private buildLayout;
46
42
  private previousMeasuredLineCount;
@@ -2,7 +2,6 @@ import { resolveToolRule } from "../../config.js";
2
2
  import { stringDisplayWidth } from "../../terminal-width.js";
3
3
  import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
4
4
  import { horizontalPaddingLayout } from "./render-text.js";
5
- import { sdkQueuedMessageEntries } from "../session/queued-message-entries.js";
6
5
  export class ConversationViewport {
7
6
  host;
8
7
  blockCachesByWidth = new Map();
@@ -63,8 +62,7 @@ export class ConversationViewport {
63
62
  return { lines: visible, changed: false };
64
63
  }
65
64
  entries() {
66
- const queued = this.queuedEntries();
67
- return queued.length === 0 ? [...this.host.entries] : [...this.host.entries, ...queued];
65
+ return [...this.host.entries];
68
66
  }
69
67
  blockForEntry(entry, width) {
70
68
  const blockCache = this.blockCacheForWidth(width);
@@ -119,21 +117,16 @@ export class ConversationViewport {
119
117
  }
120
118
  return lineCount;
121
119
  }
122
- queuedEntries() {
123
- return sdkQueuedMessageEntries(this.host.session);
124
- }
125
120
  layoutForWidth(width) {
126
- const queued = this.queuedEntries();
127
- const entries = queued.length === 0 ? this.host.entries : [...this.host.entries, ...queued];
128
- const queuedSignature = queued.map((entry) => entry.id).join("\n");
121
+ const entries = this.host.entries;
129
122
  const superCompactTools = Boolean(this.host.superCompactTools);
130
123
  const allThinkingExpanded = Boolean(this.host.allThinkingExpanded);
131
124
  let layout = this.layoutCachesByWidth.get(width);
132
- if (!layout || this.layoutStructureChanged(layout, entries, queuedSignature, superCompactTools, allThinkingExpanded)) {
133
- const previousLayout = layout && layout.queuedSignature === queuedSignature && layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
125
+ if (!layout || this.layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded)) {
126
+ const previousLayout = layout && layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
134
127
  ? layout
135
128
  : undefined;
136
- layout = this.buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded, previousLayout);
129
+ layout = this.buildLayout(entries, width, superCompactTools, allThinkingExpanded, previousLayout);
137
130
  this.layoutCachesByWidth.set(width, layout);
138
131
  }
139
132
  else {
@@ -144,7 +137,7 @@ export class ConversationViewport {
144
137
  }
145
138
  return layout;
146
139
  }
147
- buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded, previousLayout) {
140
+ buildLayout(entries, width, superCompactTools, allThinkingExpanded, previousLayout) {
148
141
  const entryIds = [];
149
142
  const lineCounts = [];
150
143
  const measuredLineCounts = [];
@@ -163,7 +156,7 @@ export class ConversationViewport {
163
156
  totalLineCount += lineCount;
164
157
  }
165
158
  offsets.push(totalLineCount);
166
- return { entries, entryIds, lineCounts, measuredLineCounts, offsets, positions, dirtyEntryIds: new Set(), totalLineCount, queuedSignature, superCompactTools, allThinkingExpanded };
159
+ return { entries, entryIds, lineCounts, measuredLineCounts, offsets, positions, dirtyEntryIds: new Set(), totalLineCount, superCompactTools, allThinkingExpanded };
167
160
  }
168
161
  previousMeasuredLineCount(previousLayout, entries, index, entry) {
169
162
  const previousIndex = previousLayout?.positions.get(entry.id);
@@ -179,8 +172,8 @@ export class ConversationViewport {
179
172
  return undefined;
180
173
  return previousLayout.lineCounts[previousIndex];
181
174
  }
182
- layoutStructureChanged(layout, entries, queuedSignature, superCompactTools, allThinkingExpanded) {
183
- if (layout.entries.length !== entries.length || layout.queuedSignature !== queuedSignature || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
175
+ layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded) {
176
+ if (layout.entries.length !== entries.length || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
184
177
  return true;
185
178
  if (layout.entries.length === 0)
186
179
  return false;
@@ -111,7 +111,7 @@ export class EditorLayoutRenderer {
111
111
  for (const [index, text] of wrapped.entries()) {
112
112
  lines.push({
113
113
  text: padHorizontalText(text, width),
114
- colorOverride: this.host.theme.colors.warning,
114
+ colorOverride: this.host.theme.colors.userForeground,
115
115
  target: { kind: "queue-message", id: entry.id },
116
116
  ...(index === 0 ? { segments: [{ start: 0, end: icon.length, foreground: this.host.theme.colors.info }] } : {}),
117
117
  });
@@ -1,5 +1,10 @@
1
1
  import type { Theme } from "../../theme.js";
2
2
  import type { ToolStatusEntry } from "../types.js";
3
+ export type WrappedTextLine = {
4
+ text: string;
5
+ copyText: string;
6
+ continuesOnNextLine?: boolean;
7
+ };
3
8
  export declare function sanitizeText(text: string): string;
4
9
  export declare function alertIconPrefixLength(text: string): number | undefined;
5
10
  export declare function normalizePastedTextForDuplicateKey(text: string): string;
@@ -12,6 +17,7 @@ export declare function toolStatusIcon(entry: ToolStatusEntry): string;
12
17
  export declare function toolStatusIconColor(entry: ToolStatusEntry, colors: Theme["colors"]): string;
13
18
  export declare function wrapLine(text: string, width: number): string[];
14
19
  export declare function wrapText(text: string, width: number): string[];
20
+ export declare function wrapTextLines(text: string, width: number): WrappedTextLine[];
15
21
  export declare function padOrTrimPlain(text: string, width: number): string;
16
22
  export declare function horizontalPaddingLayout(width: number): {
17
23
  left: number;
@@ -93,6 +93,15 @@ export function wrapText(text, width) {
93
93
  const lines = sanitizeText(text).split("\n");
94
94
  return lines.flatMap((line) => wrapLine(line, width));
95
95
  }
96
+ export function wrapTextLines(text, width) {
97
+ return sanitizeText(text)
98
+ .split("\n")
99
+ .flatMap((line) => wrapDisplayLine(line, width).map((wrapped, index, wrappedLines) => ({
100
+ text: wrapped,
101
+ copyText: wrapped,
102
+ ...(index < wrappedLines.length - 1 ? { continuesOnNextLine: true } : {}),
103
+ })));
104
+ }
96
105
  export function padOrTrimPlain(text, width) {
97
106
  return padOrTrimDisplay(text, width);
98
107
  }
@@ -3,7 +3,6 @@ import { APP_ICONS } from "../icons.js";
3
3
  import { ellipsizeDisplay } from "./render-text.js";
4
4
  const TAB_SEPARATOR = " │ ";
5
5
  const EMPTY_NEW_TAB_PREFIX = "│ ";
6
- const DEFAULT_SESSION_TITLE_PATTERN = /^session [0-9a-f]{8}$/iu;
7
6
  export const TAB_PANEL_ROWS = 2;
8
7
  export function tabPanelRows(tabLineVisible, terminalRows, tabCount = TAB_PANEL_ROWS) {
9
8
  if (!tabLineVisible)
@@ -158,10 +157,7 @@ export class TabLineRenderer {
158
157
  return this.buttonLayoutFromText(`${prefix}${ellipsizeDisplay(title, titleWidth)}${suffix}`, 0, statusText.length);
159
158
  }
160
159
  displayTitle(tab) {
161
- const title = tab.title.trim();
162
- if (!DEFAULT_SESSION_TITLE_PATTERN.test(title))
163
- return tab.title;
164
- return tab.titlePlaceholder === "loading" ? "Loading…" : "New";
160
+ return tab.title;
165
161
  }
166
162
  buttonLayoutFromText(text, statusStart, statusLength) {
167
163
  const closeStart = Math.max(0, text.lastIndexOf(APP_ICONS.close));
@@ -23,5 +23,7 @@ export type ToolBlockEntry = {
23
23
  };
24
24
  export type ToolBlockRenderOptions = {
25
25
  superCompact?: boolean;
26
+ backgroundOverride?: string;
27
+ skipHeaderBackground?: boolean;
26
28
  };
27
29
  export declare function renderToolBlock(entry: ToolBlockEntry, rule: ResolvedToolRule, width: number, colors: Theme["colors"], options?: ToolBlockRenderOptions): RenderedLine[];
@@ -10,7 +10,10 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
10
10
  const stateIcon = toolStatusIcon(entry);
11
11
  const toolColor = resolveColor(rule.color, colors);
12
12
  const toolOutputColor = colors.statusForeground;
13
- const headerLabel = entry.headerLabel ?? entry.toolName;
13
+ const headerLabel = (entry.headerLabel ?? entry.toolName).toLowerCase();
14
+ const bg = options.backgroundOverride;
15
+ const applyBackground = bg ? (lines) => { for (const line of lines)
16
+ line.backgroundOverride = bg; } : (_lines) => { };
14
17
  const headerPrefix = headerLabel ? `${stateIcon} ${headerLabel}` : stateIcon;
15
18
  const headerArgs = formatToolHeaderArgs(entry.headerArgs);
16
19
  const headerArgsWidth = width - stringDisplayWidth(headerPrefix) - 1;
@@ -22,14 +25,22 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
22
25
  text: header,
23
26
  target,
24
27
  colorOverride: toolColor,
28
+ ...(options.backgroundOverride && !options.skipHeaderBackground ? { backgroundOverride: options.backgroundOverride } : {}),
25
29
  segments: [
26
30
  { start: 0, end: stateIcon.length, foreground: toolStatusIconColor(entry, colors), bold: true },
31
+ { start: stateIcon.length, end: headerPrefix.length, bold: true },
27
32
  ...headerArgsStyledSegments(headerArgsStart, clippedHeaderArgs.length, entry.headerArgsSegments, colors),
28
33
  ],
29
34
  };
30
35
  const headerLines = [headerLine];
31
36
  if (expanded) {
32
37
  headerLines.push(...renderToolBodyLines(entry.expandedText, width, target, toolOutputColor, entry.bodyStyle, colors, entry.syntaxHighlight, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi));
38
+ if (options.skipHeaderBackground && headerLines.length > 1) {
39
+ applyBackground(headerLines.slice(1));
40
+ }
41
+ else {
42
+ applyBackground(headerLines);
43
+ }
33
44
  return headerLines;
34
45
  }
35
46
  if (rule.compactHidden || (rule.defaultExpanded === true && !options.superCompact))
@@ -39,6 +50,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
39
50
  return headerLines;
40
51
  if (!options.superCompact) {
41
52
  headerLines.push(...renderCollapsedPreviewLines(entry, body, rule, width, target, toolOutputColor, colors, hasLspDiagnostics));
53
+ applyBackground(headerLines);
42
54
  return headerLines;
43
55
  }
44
56
  const preview = collapsedInlinePreview(body, rule, entry.preserveAnsi);
@@ -105,7 +117,13 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
105
117
  ? wrapAnsiStyledDisplayLine(ansiLine, bodyWidth)
106
118
  : wrapBodyLine(displayLine, bodyWidth).map((wrapped) => ({ text: wrapped, segments: [] }));
107
119
  for (const [wrapIndex, wrapped] of wrappedLines.entries()) {
108
- const line = { text: ` ${wrapped.text}`, target, colorOverride: color };
120
+ const line = {
121
+ text: ` ${wrapped.text}`,
122
+ copyText: ` ${wrapped.text}`,
123
+ ...(wrapIndex < wrappedLines.length - 1 ? { continuesOnNextLine: true } : {}),
124
+ target,
125
+ colorOverride: color,
126
+ };
109
127
  if (diffStyle) {
110
128
  const segment = { start: 2, end: line.text.length, foreground: diffStyle.foreground };
111
129
  if (diffStyle.bold != null)
@@ -33,9 +33,11 @@ export declare function bundledSkillsInstallPath(homeDir?: string): string;
33
33
  export declare function ensurePiToolsSuiteExtensionInstalled(options?: PiToolsSuiteInstallOptions): Promise<PiToolsSuiteInstallResult>;
34
34
  export declare function ensureBundledSkillsInstalled(options?: BundledSkillsInstallOptions): Promise<BundledSkillsInstallResult>;
35
35
  export declare function getBundledExtensionPaths(): string[];
36
+ export declare function getBundledExtensionPathsAsync(): Promise<string[]>;
36
37
  export declare function prioritizeBundledQuestionExtension(base: LoadExtensionsResult, questionExtensionPath?: string): LoadExtensionsResult;
37
38
  export type CreatePixRuntimeOptions = {
38
39
  eventBus?: EventBus;
40
+ config?: PixConfig;
39
41
  };
40
42
  type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getBranch">;
41
43
  export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { cp, lstat, mkdir, readlink, realpath, rm, symlink } from "node:fs/promises";
2
+ import { access, cp, lstat, mkdir, readlink, realpath, rm, symlink } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -40,7 +40,7 @@ export function bundledSkillsInstallPath(homeDir = homedir()) {
40
40
  export async function ensurePiToolsSuiteExtensionInstalled(options = {}) {
41
41
  const sourcePath = resolve(options.sourcePath ?? piToolsSuiteExtensionSourcePath());
42
42
  const targetPath = resolve(options.targetPath ?? piToolsSuiteExtensionInstallPath(options.agentDir));
43
- if (!extensionEntryExists(sourcePath)) {
43
+ if (!(await extensionEntryExistsAsync(sourcePath))) {
44
44
  return { action: "missing-source", sourcePath, targetPath };
45
45
  }
46
46
  await mkdir(dirname(targetPath), { recursive: true });
@@ -84,6 +84,14 @@ export function getBundledExtensionPaths() {
84
84
  bundledTerminalBellExtensionPath(),
85
85
  ].filter(extensionEntryExists);
86
86
  }
87
+ export async function getBundledExtensionPathsAsync() {
88
+ const paths = await Promise.all([
89
+ bundledQuestionExtensionPath(),
90
+ bundledSessionTitleExtensionPath(),
91
+ bundledTerminalBellExtensionPath(),
92
+ ].map(async (extensionPath) => await extensionEntryExistsAsync(extensionPath) ? extensionPath : undefined));
93
+ return paths.filter((path) => path !== undefined);
94
+ }
87
95
  export function prioritizeBundledQuestionExtension(base, questionExtensionPath = bundledQuestionExtensionPath()) {
88
96
  const bundledQuestionExtensions = base.extensions.filter((extension) => isBundledQuestionExtension(extension, questionExtensionPath));
89
97
  if (bundledQuestionExtensions.length === 0)
@@ -101,6 +109,21 @@ export function prioritizeBundledQuestionExtension(base, questionExtensionPath =
101
109
  function extensionEntryExists(extensionPath) {
102
110
  return existsSync(join(extensionPath, "index.ts")) || existsSync(join(extensionPath, "index.js"));
103
111
  }
112
+ async function extensionEntryExistsAsync(extensionPath) {
113
+ try {
114
+ await access(join(extensionPath, "index.ts"));
115
+ return true;
116
+ }
117
+ catch {
118
+ try {
119
+ await access(join(extensionPath, "index.js"));
120
+ return true;
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ }
104
127
  function extensionSymlinkType() {
105
128
  return process.platform === "win32" ? "junction" : "dir";
106
129
  }
@@ -198,13 +221,13 @@ export function resolveSessionModelRefFromTail(entries) {
198
221
  export async function createPixRuntime(options, runtimeOptions = {}) {
199
222
  const agentDir = getAgentDir();
200
223
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
201
- const config = loadPixConfig(cwd);
224
+ const config = runtimeOptions.config ?? loadPixConfig(cwd);
202
225
  const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
203
226
  const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
204
227
  const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
205
228
  await ensureBundledSkillsInstalledOnce();
206
229
  await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir });
207
- const bundledExtensionPaths = getBundledExtensionPaths();
230
+ const bundledExtensionPaths = await getBundledExtensionPathsAsync();
208
231
  const services = await createAgentSessionServices({
209
232
  cwd,
210
233
  agentDir,
@@ -799,17 +799,19 @@ export class AppMouseController {
799
799
  const width = this.conversationArea()?.viewportColumns ?? this.host.terminalColumns();
800
800
  const count = range.end.line - range.start.line + 1;
801
801
  const renderedLines = this.host.conversationViewport().slice(width, range.start.line, count);
802
- const lines = [];
802
+ let copiedText = "";
803
803
  for (let index = 0; index < count; index += 1) {
804
804
  const rendered = renderedLines[index];
805
- const text = rendered?.text ?? "";
805
+ const lineText = rendered?.text ?? "";
806
806
  const line = range.start.line + index;
807
807
  const startColumn = line === range.start.line ? range.start.x : 1;
808
- const endColumn = line === range.end.line ? range.end.x : text.length + 1;
809
- const lineText = sliceByDisplayColumns(text, startColumn, endColumn);
810
- lines.push(lineText.trimEnd());
808
+ const endColumn = line === range.end.line ? range.end.x : lineText.length + 1;
809
+ const selectedLine = selectedConversationLineText(rendered, lineText, startColumn, endColumn);
810
+ copiedText += selectedLine;
811
+ if (!(rendered?.continuesOnNextLine))
812
+ copiedText += "\n";
811
813
  }
812
- return lines.join("\n").replace(/\s+$/u, "");
814
+ return copiedText.replace(/\s+$/u, "");
813
815
  }
814
816
  conversationPointFromMouse(event, clampToViewport) {
815
817
  const area = this.conversationArea();
@@ -930,6 +932,12 @@ export class AppMouseController {
930
932
  selection.moved = true;
931
933
  }
932
934
  }
935
+ function selectedConversationLineText(rendered, text, startColumn, endColumn) {
936
+ const selectsWholeLine = startColumn <= 1 && endColumn >= text.length + 1;
937
+ if (selectsWholeLine && rendered?.copyText !== undefined)
938
+ return rendered.copyText;
939
+ return sliceByDisplayColumns(text, startColumn, endColumn).trimEnd();
940
+ }
933
941
  function orderedConversationSelection(anchor, current) {
934
942
  if (anchor.line < current.line)
935
943
  return { start: anchor, end: current };
@@ -1,4 +1,4 @@
1
- import { ANSI_RESET, colorize } from "../../theme.js";
1
+ import { ANSI_RESET, ansiStylePrefix, colorize } from "../../theme.js";
2
2
  import { renderMarkdownLine } from "../../markdown-format.js";
3
3
  import { syntaxHighlightSegmentsForLine } from "../../syntax-highlight.js";
4
4
  import { displayIndexForColumn } from "../../terminal-width.js";
@@ -105,7 +105,7 @@ export class ScreenStyler {
105
105
  return chunks.join("");
106
106
  }
107
107
  styleAnsiLine(text, options) {
108
- const prefix = colorize("", options).replace(new RegExp(`${escapeRegExp(ANSI_RESET)}$`), "");
108
+ const prefix = ansiStylePrefix(options);
109
109
  if (!prefix)
110
110
  return text;
111
111
  return `${prefix}${text.replaceAll(ANSI_RESET, `${ANSI_RESET}${prefix}`)}${ANSI_RESET}`;
@@ -133,6 +133,8 @@ export class AppSessionEventController {
133
133
  case "tool_execution_end":
134
134
  this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
135
135
  this.recordToolWorkspaceMutation(event.toolCallId, event.toolName, event.result.details, event.isError);
136
+ if (this.currentUserEntryId)
137
+ this.host.scheduleUserSessionEntryMetadataSync();
136
138
  this.host.observeSubagentsToolResult(event.toolName, isRecord(event.result) ? event.result.details : undefined);
137
139
  this.host.observeTodoToolResult(event.toolName, isRecord(event.result) ? event.result.details : undefined, event.isError);
138
140
  this.upsertToolEntry(event.toolCallId, {
@@ -274,7 +276,6 @@ export class AppSessionEventController {
274
276
  this.finishCurrentThinkingEntry();
275
277
  this.flushAssistantTextBuffer(true);
276
278
  this.clearCurrentAssistantState();
277
- this.currentUserEntryId = undefined;
278
279
  }
279
280
  }
280
281
  prepareToolWorkspaceMutation(toolCallId, toolName, args) {
@@ -362,7 +363,7 @@ export class AppSessionEventController {
362
363
  }
363
364
  if (!this.assistantTextBuffer)
364
365
  return visibleText;
365
- if (shouldHoldAssistantStreamTail(this.assistantTextBuffer)) {
366
+ if (shouldHoldAssistantStreamTail(this.assistantTextBuffer, this.hasVisibleAssistantText(visibleText))) {
366
367
  if (final)
367
368
  this.assistantTextBuffer = "";
368
369
  return visibleText;
@@ -439,9 +440,9 @@ function shouldDropAssistantStreamLine(line, hasVisibleText) {
439
440
  return true;
440
441
  return isHiddenMarkdownMetadataLine(line);
441
442
  }
442
- function shouldHoldAssistantStreamTail(text) {
443
+ function shouldHoldAssistantStreamTail(text, hasVisibleText) {
443
444
  if (text.trim().length === 0)
444
- return true;
445
+ return !hasVisibleText;
445
446
  return isPotentialDcpMetadataLine(text);
446
447
  }
447
448
  function isHiddenMarkdownMetadataLine(line) {
@@ -14,6 +14,7 @@ export type AppSessionLifecycleHost = {
14
14
  inputEditor(): InputEditor;
15
15
  enableTerminal(): void;
16
16
  disposeRuntimeForQuit(runtime: AgentSessionRuntime): Promise<void>;
17
+ loadStartupConfig(): Promise<void>;
17
18
  loadRequestHistory(): Promise<void>;
18
19
  startSubagentsPolling(): void;
19
20
  closeSdkMenuForBind(): void;
@@ -67,6 +68,7 @@ export declare class AppSessionLifecycleController {
67
68
  loadSessionHistory(): void;
68
69
  requireRuntime(): AgentSessionRuntime;
69
70
  private bindSessionExtensions;
71
+ private collectAvailabilityIssues;
70
72
  private isCurrentRuntimeSession;
71
73
  private extensionUiScope;
72
74
  }
@@ -1,7 +1,6 @@
1
1
  import { createId } from "../id.js";
2
2
  import { stringifyUnknown } from "../rendering/message-content.js";
3
3
  import { collectStartupAvailabilityIssues } from "../cli/startup-checks.js";
4
- import { createStartupInfoMessage, isEmptyStartupSession } from "../cli/startup-info.js";
5
4
  export class AppSessionLifecycleController {
6
5
  host;
7
6
  unsubscribe;
@@ -16,11 +15,17 @@ export class AppSessionLifecycleController {
16
15
  throw new Error("pi-ui-extend needs an interactive TTY");
17
16
  }
18
17
  this.host.enableTerminal();
19
- await this.host.loadRequestHistory();
20
18
  this.host.setRunning(true);
21
19
  this.host.startSubagentsPolling();
22
20
  this.host.render();
21
+ void this.host.loadRequestHistory().catch((error) => {
22
+ if (!this.host.isRunning())
23
+ return;
24
+ this.host.addEntry({ id: createId("warning"), kind: "system", text: `Request history failed to load: ${stringifyUnknown(error)}` });
25
+ this.host.render();
26
+ });
23
27
  try {
28
+ await this.host.loadStartupConfig();
24
29
  const runtime = await this.host.createRuntime();
25
30
  if (!this.host.isRunning()) {
26
31
  await this.host.disposeRuntimeForQuit(runtime);
@@ -31,24 +36,6 @@ export class AppSessionLifecycleController {
31
36
  await this.bindCurrentSession({ awaitExtensions: false });
32
37
  });
33
38
  await this.bindCurrentSession({ awaitExtensions: false });
34
- if (isEmptyStartupSession(runtime)) {
35
- this.host.addEntry({ id: createId("system"), kind: "system", text: createStartupInfoMessage(runtime) });
36
- }
37
- await this.host.restoreTabsAfterStartup();
38
- const availabilityIssues = await collectStartupAvailabilityIssues(runtime);
39
- for (const issue of availabilityIssues) {
40
- this.host.addEntry({
41
- id: createId(issue.kind),
42
- kind: issue.kind === "error" ? "error" : "system",
43
- text: issue.message,
44
- });
45
- }
46
- if (availabilityIssues.some((issue) => issue.kind === "error")) {
47
- this.host.showToast("Startup dependency unavailable", "error");
48
- }
49
- else if (availabilityIssues.length > 0) {
50
- this.host.showToast("Startup dependency warning", "warning");
51
- }
52
39
  if (runtime.modelFallbackMessage) {
53
40
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
54
41
  }
@@ -59,6 +46,14 @@ export class AppSessionLifecycleController {
59
46
  this.host.setSessionStatus(runtime.session);
60
47
  this.host.setSessionActivity(runtime.session.isStreaming ? "running" : "idle");
61
48
  this.host.render();
49
+ void this.collectAvailabilityIssues(runtime);
50
+ void this.host.restoreTabsAfterStartup().catch((error) => {
51
+ if (!this.host.isRunning())
52
+ return;
53
+ this.host.addEntry({ id: createId("warning"), kind: "system", text: `Tab restore failed: ${stringifyUnknown(error)}` });
54
+ this.host.showToast("Could not restore tabs", "warning");
55
+ this.host.render();
56
+ });
62
57
  }
63
58
  catch (error) {
64
59
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
@@ -154,6 +149,34 @@ export class AppSessionLifecycleController {
154
149
  this.extensionBindSession = session;
155
150
  return promise;
156
151
  }
152
+ async collectAvailabilityIssues(runtime) {
153
+ try {
154
+ const availabilityIssues = await collectStartupAvailabilityIssues(runtime);
155
+ if (!this.host.isRunning() || this.host.runtime() !== runtime)
156
+ return;
157
+ for (const issue of availabilityIssues) {
158
+ this.host.addEntry({
159
+ id: createId(issue.kind),
160
+ kind: issue.kind === "error" ? "error" : "system",
161
+ text: issue.message,
162
+ });
163
+ }
164
+ if (availabilityIssues.some((issue) => issue.kind === "error")) {
165
+ this.host.showToast("Startup dependency unavailable", "error");
166
+ }
167
+ else if (availabilityIssues.length > 0) {
168
+ this.host.showToast("Startup dependency warning", "warning");
169
+ }
170
+ if (availabilityIssues.length > 0)
171
+ this.host.render();
172
+ }
173
+ catch (error) {
174
+ if (!this.host.isRunning() || this.host.runtime() !== runtime)
175
+ return;
176
+ this.host.addEntry({ id: createId("warning"), kind: "system", text: `Startup dependency check failed: ${stringifyUnknown(error)}` });
177
+ this.host.render();
178
+ }
179
+ }
157
180
  isCurrentRuntimeSession(runtime, session) {
158
181
  return this.host.isRunning() && this.host.runtime() === runtime && runtime.session === session;
159
182
  }
@@ -6,7 +6,7 @@ import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessa
6
6
  export type TabInputState = InputEditorDraftState;
7
7
  export type AppTabsControllerHost = {
8
8
  readonly options: AppOptions;
9
- readonly maxProjectSessions?: number;
9
+ readonly maxProjectSessions?: number | (() => number | undefined);
10
10
  readonly blinkController: AppBlinkController;
11
11
  runtime(): AgentSessionRuntime | undefined;
12
12
  createRuntimeForNewSession(): Promise<AgentSessionRuntime>;
@@ -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;