pi-ui-extend 0.1.32 → 0.1.34

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 (95) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +2 -0
  3. package/dist/app/app.js +28 -0
  4. package/dist/app/commands/command-session-actions.js +29 -1
  5. package/dist/app/constants.d.ts +1 -1
  6. package/dist/app/constants.js +2 -2
  7. package/dist/app/icons.d.ts +4 -9
  8. package/dist/app/icons.js +12 -35
  9. package/dist/app/model/model-usage-status.d.ts +2 -1
  10. package/dist/app/model/model-usage-status.js +33 -25
  11. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  12. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  13. package/dist/app/rendering/conversation-tool-renderer.js +12 -18
  14. package/dist/app/rendering/conversation-viewport.d.ts +4 -0
  15. package/dist/app/rendering/conversation-viewport.js +144 -13
  16. package/dist/app/rendering/dcp-stats.js +42 -16
  17. package/dist/app/rendering/render-controller.js +4 -0
  18. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  19. package/dist/app/rendering/status-line-renderer.js +36 -1
  20. package/dist/app/rendering/tab-line-renderer.js +2 -2
  21. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  22. package/dist/app/rendering/tool-block-renderer.js +37 -11
  23. package/dist/app/runtime.js +1 -1
  24. package/dist/app/screen/mouse-controller.d.ts +5 -1
  25. package/dist/app/screen/mouse-controller.js +16 -0
  26. package/dist/app/screen/scroll-controller.d.ts +20 -0
  27. package/dist/app/screen/scroll-controller.js +127 -10
  28. package/dist/app/session/lazy-session-manager.js +35 -5
  29. package/dist/app/session/pix-system-message.d.ts +1 -0
  30. package/dist/app/session/pix-system-message.js +14 -3
  31. package/dist/app/session/queued-message-controller.d.ts +11 -4
  32. package/dist/app/session/queued-message-controller.js +74 -59
  33. package/dist/app/session/queued-message-entries.d.ts +2 -1
  34. package/dist/app/session/queued-message-entries.js +12 -1
  35. package/dist/app/session/session-event-controller.d.ts +42 -1
  36. package/dist/app/session/session-event-controller.js +500 -31
  37. package/dist/app/session/session-history.js +23 -4
  38. package/dist/app/session/tabs-controller.d.ts +11 -1
  39. package/dist/app/session/tabs-controller.js +102 -21
  40. package/dist/app/types.d.ts +14 -1
  41. package/dist/bundled-extensions/question/contract.d.ts +25 -0
  42. package/dist/bundled-extensions/question/contract.js +94 -0
  43. package/dist/bundled-extensions/question/index.d.ts +7 -0
  44. package/dist/bundled-extensions/question/index.js +28 -0
  45. package/dist/bundled-extensions/question/render.d.ts +4 -0
  46. package/dist/bundled-extensions/question/render.js +27 -0
  47. package/dist/bundled-extensions/question/result.d.ts +6 -0
  48. package/dist/bundled-extensions/question/result.js +84 -0
  49. package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
  50. package/dist/bundled-extensions/question/tool-description.js +11 -0
  51. package/dist/bundled-extensions/question/tui.d.ts +2 -0
  52. package/dist/bundled-extensions/question/tui.js +577 -0
  53. package/dist/bundled-extensions/question/types.d.ts +103 -0
  54. package/dist/bundled-extensions/question/types.js +1 -0
  55. package/dist/bundled-extensions/session-title/config.d.ts +17 -0
  56. package/dist/bundled-extensions/session-title/config.js +150 -0
  57. package/dist/bundled-extensions/session-title/index.d.ts +5 -0
  58. package/dist/bundled-extensions/session-title/index.js +384 -0
  59. package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
  60. package/dist/bundled-extensions/session-title/title-generation.js +141 -0
  61. package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
  62. package/dist/bundled-extensions/terminal-bell/index.js +491 -0
  63. package/dist/config.d.ts +1 -1
  64. package/dist/config.js +2 -1
  65. package/dist/default-pix-config.js +2 -1
  66. package/dist/icon-theme.d.ts +7 -0
  67. package/dist/icon-theme.js +36 -0
  68. package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
  69. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  70. package/dist/schemas/pix-schema.d.ts +1 -0
  71. package/dist/schemas/pix-schema.js +1 -0
  72. package/external/pi-tools-suite/README.md +7 -7
  73. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
  74. package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
  75. package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
  76. package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
  77. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
  78. package/external/pi-tools-suite/src/dcp/config.ts +14 -14
  79. package/external/pi-tools-suite/src/dcp/index.ts +31 -43
  80. package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
  81. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
  82. package/external/pi-tools-suite/src/tool-descriptions.ts +34 -54
  83. package/package.json +3 -2
  84. package/schemas/pi-tools-suite.json +14 -0
  85. package/schemas/pix.json +7 -0
  86. package/extensions/question/contract.ts +0 -100
  87. package/extensions/question/index.ts +0 -34
  88. package/extensions/question/render.ts +0 -28
  89. package/extensions/question/result.ts +0 -86
  90. package/extensions/question/tool-description.ts +0 -11
  91. package/extensions/question/tui.ts +0 -629
  92. package/extensions/question/types.ts +0 -123
  93. package/extensions/session-title/config.ts +0 -164
  94. package/extensions/session-title/index.ts +0 -502
  95. package/extensions/terminal-bell/index.ts +0 -345
@@ -0,0 +1,150 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, parse, resolve } from "node:path";
4
+ import { parse as parseJsonc } from "jsonc-parser";
5
+ const DEFAULT_CONFIG = {
6
+ enabled: true,
7
+ model: "zai/glm-4.5-air",
8
+ fallbackModels: [],
9
+ maxInputChars: 2000,
10
+ maxTitleChars: 80,
11
+ maxTokens: 32,
12
+ maxRetries: 2,
13
+ generationAttempts: 3,
14
+ retryDelayMs: 3000,
15
+ timeoutMs: 12_000,
16
+ terminalTitle: true,
17
+ terminalTitlePrefix: "pi — ",
18
+ notify: false,
19
+ debug: false,
20
+ };
21
+ const PIX_CONFIG_FILE = "pix.jsonc";
22
+ const SESSION_TITLE_CONFIG_FILE = "session-title.jsonc";
23
+ function isRecord(value) {
24
+ return value !== null && typeof value === "object" && !Array.isArray(value);
25
+ }
26
+ function readJsonc(filePath) {
27
+ if (!existsSync(filePath))
28
+ return {};
29
+ try {
30
+ const parsed = parseJsonc(readFileSync(filePath, "utf8"));
31
+ return isRecord(parsed) ? parsed : {};
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ function mergeConfig(base, raw) {
38
+ const next = { ...base };
39
+ if (typeof raw.enabled === "boolean")
40
+ next.enabled = raw.enabled;
41
+ const model = readNonEmptyString(raw.modelRef) ?? readNonEmptyString(raw.model);
42
+ if (model)
43
+ next.model = model;
44
+ const fallbackModels = readModelList(raw.fallbackModels);
45
+ if (fallbackModels)
46
+ next.fallbackModels = fallbackModels;
47
+ if (typeof raw.terminalTitle === "boolean")
48
+ next.terminalTitle = raw.terminalTitle;
49
+ if (typeof raw.terminalTitlePrefix === "string")
50
+ next.terminalTitlePrefix = raw.terminalTitlePrefix;
51
+ if (typeof raw.notify === "boolean")
52
+ next.notify = raw.notify;
53
+ if (typeof raw.debug === "boolean")
54
+ next.debug = raw.debug;
55
+ if (typeof raw.maxInputChars === "number" && Number.isFinite(raw.maxInputChars)) {
56
+ next.maxInputChars = Math.max(100, Math.floor(raw.maxInputChars));
57
+ }
58
+ if (typeof raw.maxTitleChars === "number" && Number.isFinite(raw.maxTitleChars)) {
59
+ next.maxTitleChars = Math.max(20, Math.floor(raw.maxTitleChars));
60
+ }
61
+ if (typeof raw.maxTokens === "number" && Number.isFinite(raw.maxTokens)) {
62
+ next.maxTokens = Math.max(8, Math.floor(raw.maxTokens));
63
+ }
64
+ if (typeof raw.maxRetries === "number" && Number.isFinite(raw.maxRetries)) {
65
+ next.maxRetries = Math.max(0, Math.floor(raw.maxRetries));
66
+ }
67
+ if (typeof raw.generationAttempts === "number" && Number.isFinite(raw.generationAttempts)) {
68
+ next.generationAttempts = Math.max(1, Math.floor(raw.generationAttempts));
69
+ }
70
+ if (typeof raw.retryDelayMs === "number" && Number.isFinite(raw.retryDelayMs)) {
71
+ next.retryDelayMs = Math.max(250, Math.floor(raw.retryDelayMs));
72
+ }
73
+ if (typeof raw.timeoutMs === "number" && Number.isFinite(raw.timeoutMs)) {
74
+ next.timeoutMs = Math.max(1000, Math.floor(raw.timeoutMs));
75
+ }
76
+ return next;
77
+ }
78
+ function readNonEmptyString(value) {
79
+ if (typeof value !== "string")
80
+ return undefined;
81
+ const trimmed = value.trim();
82
+ return trimmed.length > 0 ? trimmed : undefined;
83
+ }
84
+ function readModelList(value) {
85
+ if (!Array.isArray(value))
86
+ return undefined;
87
+ return value
88
+ .map((item) => readNonEmptyString(item))
89
+ .filter((item) => item !== undefined);
90
+ }
91
+ function readPixSessionTitleConfig(configDir) {
92
+ const pixConfig = readJsonc(join(configDir, PIX_CONFIG_FILE));
93
+ return isRecord(pixConfig.sessionTitle) ? pixConfig.sessionTitle : {};
94
+ }
95
+ function applyEnv(config) {
96
+ let next = { ...config };
97
+ if (["1", "true", "on", "yes"].includes((process.env.PI_OFFLINE ?? "").trim().toLowerCase())) {
98
+ next.enabled = false;
99
+ }
100
+ const enabled = process.env.PI_SESSION_TITLE_ENABLED;
101
+ if (enabled !== undefined)
102
+ next.enabled = !["0", "false", "off", "no"].includes(enabled.trim().toLowerCase());
103
+ const model = process.env.PI_SESSION_TITLE_MODEL;
104
+ if (model?.trim())
105
+ next.model = model.trim();
106
+ if (process.env.PI_SESSION_TITLE_TERMINAL_TITLE !== undefined) {
107
+ next.terminalTitle = ["1", "true", "on", "yes"].includes(process.env.PI_SESSION_TITLE_TERMINAL_TITLE.trim().toLowerCase());
108
+ }
109
+ if (process.env.PI_SESSION_TITLE_TERMINAL_PREFIX !== undefined) {
110
+ next.terminalTitlePrefix = process.env.PI_SESSION_TITLE_TERMINAL_PREFIX;
111
+ }
112
+ if (process.env.PI_SESSION_TITLE_NOTIFY !== undefined) {
113
+ next.notify = ["1", "true", "on", "yes"].includes(process.env.PI_SESSION_TITLE_NOTIFY.trim().toLowerCase());
114
+ }
115
+ if (process.env.PI_SESSION_TITLE_DEBUG !== undefined) {
116
+ next.debug = ["1", "true", "on", "yes"].includes(process.env.PI_SESSION_TITLE_DEBUG.trim().toLowerCase());
117
+ }
118
+ return next;
119
+ }
120
+ function findProjectConfig(startDir) {
121
+ let dir = resolve(startDir);
122
+ const root = parse(dir).root;
123
+ while (true) {
124
+ const candidate = join(dir, ".pi", "session-title.jsonc");
125
+ if (existsSync(candidate))
126
+ return candidate;
127
+ if (dir === root)
128
+ return undefined;
129
+ const parent = dirname(dir);
130
+ if (parent === dir)
131
+ return undefined;
132
+ dir = parent;
133
+ }
134
+ }
135
+ export function loadSessionTitleConfig(projectDir) {
136
+ let config = { ...DEFAULT_CONFIG };
137
+ const homeConfigDir = join(homedir(), ".config", "pi");
138
+ config = mergeConfig(config, readPixSessionTitleConfig(homeConfigDir));
139
+ config = mergeConfig(config, readJsonc(join(homeConfigDir, SESSION_TITLE_CONFIG_FILE)));
140
+ const piConfigDir = process.env.PI_CONFIG_DIR;
141
+ if (piConfigDir) {
142
+ config = mergeConfig(config, readPixSessionTitleConfig(piConfigDir));
143
+ config = mergeConfig(config, readJsonc(join(piConfigDir, SESSION_TITLE_CONFIG_FILE)));
144
+ }
145
+ const projectConfig = findProjectConfig(projectDir);
146
+ if (projectConfig) {
147
+ config = mergeConfig(config, readJsonc(projectConfig));
148
+ }
149
+ return applyEnv(config);
150
+ }
@@ -0,0 +1,5 @@
1
+ import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export { fallbackSessionTitleFromInput, generateSessionTitle, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
3
+ export declare function firstUserMessageText(ctx: ExtensionContext): string | undefined;
4
+ export declare function buildForkTitleInput(parentTitle: string | undefined, forkPrompt: string): string;
5
+ export default function sessionTitle(pi: ExtensionAPI): void;
@@ -0,0 +1,384 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import { resolve } from "node:path";
3
+ import { loadSessionTitleConfig } from "./config.js";
4
+ import { fallbackSessionTitleFromInput, firstUserMessageText as firstUserMessageTextFromEntries, generateSessionTitle, sessionTitleModelRefs, } from "./title-generation.js";
5
+ export { fallbackSessionTitleFromInput, generateSessionTitle, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
6
+ const DEFAULT_TERMINAL_TITLE = "pi";
7
+ function imageAttachmentLabel(images) {
8
+ if (images.length === 0)
9
+ return undefined;
10
+ return images.length === 1 ? "Attached image" : `Attached images (${images.length})`;
11
+ }
12
+ function fallbackTitleInputFromPrompt(text, images = []) {
13
+ const trimmedText = text.trim();
14
+ return trimmedText || imageAttachmentLabel(images);
15
+ }
16
+ function titleGenerationInputFromPrompt(text, images = []) {
17
+ const trimmedText = text.trim();
18
+ const imageLabel = imageAttachmentLabel(images);
19
+ if (trimmedText && imageLabel)
20
+ return `${trimmedText}\n\n${imageLabel}`;
21
+ return trimmedText || imageLabel;
22
+ }
23
+ export function firstUserMessageText(ctx) {
24
+ return firstUserMessageTextFromEntries(ctx.sessionManager.getBranch());
25
+ }
26
+ function hasExistingUserMessage(ctx) {
27
+ return firstUserMessageText(ctx) !== undefined;
28
+ }
29
+ function truncateInput(text, maxChars) {
30
+ const trimmed = text.trim();
31
+ if (trimmed.length <= maxChars)
32
+ return trimmed;
33
+ return `${trimmed.slice(0, maxChars).trimEnd()}…`;
34
+ }
35
+ function terminalSafeText(text) {
36
+ return text
37
+ .replace(/[\u0000-\u001f\u007f]/gu, " ")
38
+ .replace(/\s+/gu, " ")
39
+ .trim();
40
+ }
41
+ export function buildForkTitleInput(parentTitle, forkPrompt) {
42
+ const prompt = forkPrompt.trim();
43
+ const parent = parentTitle?.trim();
44
+ if (!parent)
45
+ return prompt;
46
+ return [
47
+ "Parent session title:",
48
+ parent,
49
+ "",
50
+ "First prompt in this fork:",
51
+ prompt,
52
+ ].join("\n");
53
+ }
54
+ export default function sessionTitle(pi) {
55
+ let config;
56
+ let sessionId;
57
+ let controller;
58
+ let lastRenderedName;
59
+ let lastRenderedTitle;
60
+ let retryTimer;
61
+ const refreshTimers = new Set();
62
+ let pendingGeneration;
63
+ let forkTitleState;
64
+ function abortCurrentRequest() {
65
+ controller?.abort();
66
+ controller = undefined;
67
+ }
68
+ function clearRetryTimer() {
69
+ if (!retryTimer)
70
+ return;
71
+ clearTimeout(retryTimer);
72
+ retryTimer = undefined;
73
+ }
74
+ function clearRefreshTimers() {
75
+ for (const timer of refreshTimers)
76
+ clearTimeout(timer);
77
+ refreshTimers.clear();
78
+ }
79
+ function currentSessionName(ctx) {
80
+ const name = pi.getSessionName() ?? ctx?.sessionManager.getSessionName?.();
81
+ return name?.trim() || undefined;
82
+ }
83
+ function shouldGeneratePendingTitle(ctx) {
84
+ if (!pendingGeneration)
85
+ return false;
86
+ if (pendingGeneration.sessionId !== ctx.sessionManager.getSessionId())
87
+ return false;
88
+ const name = currentSessionName(ctx);
89
+ if (!name)
90
+ return true;
91
+ if (pendingGeneration.provisionalSessionName && name === pendingGeneration.provisionalSessionName)
92
+ return true;
93
+ return Boolean(pendingGeneration.replaceSessionName && name === pendingGeneration.replaceSessionName);
94
+ }
95
+ function advancePendingGeneration(currentConfig) {
96
+ if (!pendingGeneration)
97
+ return false;
98
+ while (pendingGeneration.attempts >= currentConfig.generationAttempts) {
99
+ if (pendingGeneration.modelIndex >= pendingGeneration.modelRefs.length - 1)
100
+ return false;
101
+ pendingGeneration.modelIndex++;
102
+ pendingGeneration.attempts = 0;
103
+ }
104
+ return pendingGeneration.modelIndex < pendingGeneration.modelRefs.length;
105
+ }
106
+ function renderTerminalTitle(ctx, name, force = false) {
107
+ if (!ctx.hasUI || !config?.enabled || !config.terminalTitle)
108
+ return;
109
+ const title = name ? `${config.terminalTitlePrefix}${name}` : DEFAULT_TERMINAL_TITLE;
110
+ const safeTitle = terminalSafeText(title) || DEFAULT_TERMINAL_TITLE;
111
+ if (!force && safeTitle === lastRenderedTitle)
112
+ return;
113
+ ctx.ui.setTitle(safeTitle);
114
+ lastRenderedTitle = safeTitle;
115
+ }
116
+ function refreshSessionUi(ctx, options = {}) {
117
+ const name = currentSessionName(ctx);
118
+ const nameChanged = name !== lastRenderedName;
119
+ if (options.force || nameChanged) {
120
+ lastRenderedName = name;
121
+ }
122
+ if (options.force || options.reapplyTitle || nameChanged) {
123
+ renderTerminalTitle(ctx, name, options.force || options.reapplyTitle);
124
+ }
125
+ }
126
+ function scheduleSessionUiRefresh(ctx) {
127
+ if (!ctx.hasUI)
128
+ return;
129
+ clearRefreshTimers();
130
+ for (const delayMs of [0, 100, 500, 1500, 3000]) {
131
+ const timer = setTimeout(() => {
132
+ refreshTimers.delete(timer);
133
+ refreshSessionUi(ctx, { reapplyTitle: true });
134
+ }, delayMs);
135
+ timer.unref?.();
136
+ refreshTimers.add(timer);
137
+ }
138
+ }
139
+ function scheduleGenerationRetry(ctx, currentConfig) {
140
+ clearRetryTimer();
141
+ if (!pendingGeneration)
142
+ return;
143
+ if (!shouldGeneratePendingTitle(ctx))
144
+ return;
145
+ if (!advancePendingGeneration(currentConfig))
146
+ return;
147
+ retryTimer = setTimeout(() => {
148
+ retryTimer = undefined;
149
+ startTitleGeneration(ctx, currentConfig);
150
+ }, currentConfig.retryDelayMs);
151
+ retryTimer.unref?.();
152
+ }
153
+ function applyFallbackSessionTitle(ctx, currentConfig, input, options = {}) {
154
+ const currentName = currentSessionName(ctx);
155
+ if (!options.force && currentName)
156
+ return false;
157
+ const fallbackTitle = fallbackSessionTitleFromInput(input, currentConfig.maxTitleChars);
158
+ if (!fallbackTitle)
159
+ return false;
160
+ pi.setSessionName(fallbackTitle);
161
+ refreshSessionUi(ctx, { force: true });
162
+ scheduleSessionUiRefresh(ctx);
163
+ return true;
164
+ }
165
+ function startTitleGeneration(ctx, currentConfig) {
166
+ if (!pendingGeneration)
167
+ return;
168
+ if (controller)
169
+ return;
170
+ if (!shouldGeneratePendingTitle(ctx))
171
+ return;
172
+ if (!advancePendingGeneration(currentConfig)) {
173
+ applyFallbackSessionTitle(ctx, currentConfig, pendingGeneration.input, {
174
+ force: Boolean(pendingGeneration.replaceSessionName),
175
+ });
176
+ pendingGeneration = undefined;
177
+ return;
178
+ }
179
+ const modelRef = pendingGeneration.modelRefs[pendingGeneration.modelIndex];
180
+ if (!modelRef) {
181
+ pendingGeneration = undefined;
182
+ return;
183
+ }
184
+ pendingGeneration.attempts++;
185
+ abortCurrentRequest();
186
+ controller = new AbortController();
187
+ const requestController = controller;
188
+ const currentSessionId = pendingGeneration.sessionId;
189
+ const generation = { ...pendingGeneration, modelRef };
190
+ void (async () => {
191
+ try {
192
+ const title = await generateSessionTitle(generation.input, ctx.modelRegistry, currentConfig, generation.modelRef, requestController.signal, currentConfig.debug && ctx.hasUI ? (message) => ctx.ui.notify(message, "warning") : undefined);
193
+ if (!title || requestController.signal.aborted)
194
+ return;
195
+ if (sessionId !== currentSessionId)
196
+ return;
197
+ if (!shouldGeneratePendingTitle(ctx))
198
+ return;
199
+ pi.setSessionName(title);
200
+ pendingGeneration = undefined;
201
+ refreshSessionUi(ctx, { force: true });
202
+ scheduleSessionUiRefresh(ctx);
203
+ if (currentConfig.notify && ctx.hasUI)
204
+ ctx.ui.notify(`Session named: ${title}`, "info");
205
+ }
206
+ catch (error) {
207
+ if (requestController.signal.aborted)
208
+ return;
209
+ if (currentConfig.debug && ctx.hasUI) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ ctx.ui.notify(`Session title generation failed: ${message}`, "warning");
212
+ }
213
+ }
214
+ finally {
215
+ if (controller === requestController)
216
+ controller = undefined;
217
+ if (requestController.signal.aborted || pendingGeneration?.sessionId !== currentSessionId)
218
+ return;
219
+ if (shouldGeneratePendingTitle(ctx)) {
220
+ if (!advancePendingGeneration(currentConfig)) {
221
+ applyFallbackSessionTitle(ctx, currentConfig, generation.input, {
222
+ force: Boolean(generation.replaceSessionName),
223
+ });
224
+ pendingGeneration = undefined;
225
+ return;
226
+ }
227
+ scheduleGenerationRetry(ctx, currentConfig);
228
+ return;
229
+ }
230
+ }
231
+ })();
232
+ }
233
+ function isSameSessionPath(left, right) {
234
+ if (!left || !right)
235
+ return false;
236
+ if (left === right)
237
+ return true;
238
+ try {
239
+ return resolve(left) === resolve(right);
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ async function resolveParentSessionTitle(options) {
246
+ const { parentSessionFile, cwd, sessionDir, fallbackTitle } = options;
247
+ if (!parentSessionFile)
248
+ return fallbackTitle;
249
+ try {
250
+ const directParentName = SessionManager.open(parentSessionFile).getSessionName()?.trim();
251
+ if (directParentName)
252
+ return directParentName;
253
+ const sessions = await SessionManager.list(cwd, sessionDir);
254
+ const parent = sessions.find((info) => isSameSessionPath(info.path, parentSessionFile));
255
+ const parentName = parent?.name?.trim();
256
+ if (parentName)
257
+ return parentName;
258
+ }
259
+ catch {
260
+ // Ignore lookup failures and keep the inherited session name as fallback.
261
+ }
262
+ return fallbackTitle;
263
+ }
264
+ function prepareForkTitleState(event, ctx) {
265
+ forkTitleState = undefined;
266
+ if (event.reason !== "fork")
267
+ return;
268
+ const currentSessionId = ctx.sessionManager.getSessionId();
269
+ const inheritedSessionName = currentSessionName(ctx);
270
+ const parentSessionFile = event.previousSessionFile ?? ctx.sessionManager.getHeader()?.parentSession;
271
+ const cwd = ctx.cwd;
272
+ const sessionDir = ctx.sessionManager.getSessionDir();
273
+ forkTitleState = {
274
+ sessionId: currentSessionId,
275
+ parentTitle: inheritedSessionName,
276
+ inheritedSessionName,
277
+ };
278
+ void resolveParentSessionTitle({
279
+ parentSessionFile,
280
+ cwd,
281
+ sessionDir,
282
+ fallbackTitle: inheritedSessionName,
283
+ }).then((parentTitle) => {
284
+ if (sessionId !== currentSessionId)
285
+ return;
286
+ if (forkTitleState?.sessionId !== currentSessionId)
287
+ return;
288
+ forkTitleState = {
289
+ sessionId: currentSessionId,
290
+ parentTitle,
291
+ inheritedSessionName,
292
+ };
293
+ });
294
+ }
295
+ pi.on("session_start", async (event, ctx) => {
296
+ abortCurrentRequest();
297
+ clearRetryTimer();
298
+ clearRefreshTimers();
299
+ config = loadSessionTitleConfig(ctx.cwd);
300
+ sessionId = ctx.sessionManager.getSessionId();
301
+ pendingGeneration = undefined;
302
+ forkTitleState = undefined;
303
+ lastRenderedName = undefined;
304
+ lastRenderedTitle = undefined;
305
+ refreshSessionUi(ctx, { force: true });
306
+ scheduleSessionUiRefresh(ctx);
307
+ prepareForkTitleState(event, ctx);
308
+ });
309
+ pi.on("session_shutdown", async () => {
310
+ abortCurrentRequest();
311
+ clearRetryTimer();
312
+ clearRefreshTimers();
313
+ forkTitleState = undefined;
314
+ });
315
+ function refreshOnEvent(ctx) {
316
+ refreshSessionUi(ctx);
317
+ scheduleSessionUiRefresh(ctx);
318
+ }
319
+ pi.on("agent_start", async (_event, ctx) => refreshOnEvent(ctx));
320
+ pi.on("agent_end", async (_event, ctx) => refreshOnEvent(ctx));
321
+ pi.on("turn_start", async (_event, ctx) => refreshOnEvent(ctx));
322
+ pi.on("turn_end", async (_event, ctx) => refreshOnEvent(ctx));
323
+ pi.on("session_tree", async (_event, ctx) => refreshOnEvent(ctx));
324
+ pi.on("session_compact", async (_event, ctx) => refreshOnEvent(ctx));
325
+ pi.on("input", async (event, ctx) => {
326
+ const currentConfig = config ?? loadSessionTitleConfig(ctx.cwd);
327
+ config = currentConfig;
328
+ refreshSessionUi(ctx);
329
+ scheduleSessionUiRefresh(ctx);
330
+ if (event.source === "extension")
331
+ return { action: "continue" };
332
+ const fallbackInput = fallbackTitleInputFromPrompt(event.text, event.images);
333
+ if (!fallbackInput)
334
+ return { action: "continue" };
335
+ const titleInput = titleGenerationInputFromPrompt(event.text, event.images) ?? fallbackInput;
336
+ if (event.text.trimStart().startsWith("/"))
337
+ return { action: "continue" };
338
+ const currentSessionId = ctx.sessionManager.getSessionId();
339
+ sessionId = currentSessionId;
340
+ const currentName = currentSessionName(ctx);
341
+ const activeForkTitleState = forkTitleState?.sessionId === currentSessionId ? forkTitleState : undefined;
342
+ if (!activeForkTitleState && hasExistingUserMessage(ctx)) {
343
+ forkTitleState = undefined;
344
+ return { action: "continue" };
345
+ }
346
+ if (currentName && (!activeForkTitleState || currentName !== activeForkTitleState.inheritedSessionName)) {
347
+ forkTitleState = undefined;
348
+ return { action: "continue" };
349
+ }
350
+ if (!currentConfig.enabled) {
351
+ applyFallbackSessionTitle(ctx, currentConfig, activeForkTitleState
352
+ ? buildForkTitleInput(activeForkTitleState.parentTitle, fallbackInput)
353
+ : fallbackInput, { force: Boolean(activeForkTitleState) });
354
+ forkTitleState = undefined;
355
+ return { action: "continue" };
356
+ }
357
+ if (!pendingGeneration || pendingGeneration.sessionId !== currentSessionId) {
358
+ const input = activeForkTitleState
359
+ ? buildForkTitleInput(activeForkTitleState.parentTitle, titleInput)
360
+ : titleInput;
361
+ const fallbackTitleInput = activeForkTitleState
362
+ ? buildForkTitleInput(activeForkTitleState.parentTitle, fallbackInput)
363
+ : fallbackInput;
364
+ const provisionalSessionName = fallbackSessionTitleFromInput(fallbackTitleInput, currentConfig.maxTitleChars);
365
+ if (provisionalSessionName && (!currentName || activeForkTitleState)) {
366
+ pi.setSessionName(provisionalSessionName);
367
+ refreshSessionUi(ctx, { force: true });
368
+ scheduleSessionUiRefresh(ctx);
369
+ }
370
+ pendingGeneration = {
371
+ sessionId: currentSessionId,
372
+ input: truncateInput(input, currentConfig.maxInputChars),
373
+ modelRefs: sessionTitleModelRefs(currentConfig),
374
+ modelIndex: 0,
375
+ attempts: 0,
376
+ ...(activeForkTitleState?.inheritedSessionName === undefined ? {} : { replaceSessionName: activeForkTitleState.inheritedSessionName }),
377
+ ...(provisionalSessionName === undefined ? {} : { provisionalSessionName }),
378
+ };
379
+ forkTitleState = undefined;
380
+ }
381
+ startTitleGeneration(ctx, currentConfig);
382
+ return { action: "continue" };
383
+ });
384
+ }
@@ -0,0 +1,26 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { SessionTitleConfig } from "./config.js";
3
+ type SessionEntryLike = {
4
+ type?: string;
5
+ message?: {
6
+ role?: string;
7
+ content?: unknown;
8
+ };
9
+ };
10
+ type TitleModelRegistry = {
11
+ find(provider: string, modelId: string): Model<Api> | undefined;
12
+ getApiKeyAndHeaders(model: Model<Api>): Promise<{
13
+ ok: true;
14
+ apiKey?: string;
15
+ headers?: Record<string, string>;
16
+ } | {
17
+ ok: false;
18
+ error: string;
19
+ }>;
20
+ };
21
+ export declare function firstUserMessageText(entries: readonly SessionEntryLike[]): string | undefined;
22
+ export declare function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined;
23
+ export declare function sanitizeSessionTitle(raw: string, maxTitleChars: number): string | undefined;
24
+ export declare function sessionTitleModelRefs(config: SessionTitleConfig): string[];
25
+ export declare function generateSessionTitle(input: string, modelRegistry: TitleModelRegistry, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
26
+ export {};