pi-ui-extend 0.1.34 → 0.1.35

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 (58) hide show
  1. package/README.md +20 -0
  2. package/dist/app/app.js +4 -2
  3. package/dist/app/constants.d.ts +2 -1
  4. package/dist/app/constants.js +6 -1
  5. package/dist/app/input/input-controller.d.ts +4 -1
  6. package/dist/app/input/input-controller.js +95 -16
  7. package/dist/app/input/input-paste-handler.js +3 -1
  8. package/dist/app/input/terminal-edit-shortcuts.d.ts +20 -0
  9. package/dist/app/input/terminal-edit-shortcuts.js +50 -16
  10. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  11. package/dist/app/rendering/conversation-entry-renderer.js +1 -1
  12. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  13. package/dist/app/rendering/conversation-tool-renderer.js +21 -0
  14. package/dist/app/rendering/conversation-viewport.d.ts +3 -0
  15. package/dist/app/rendering/conversation-viewport.js +41 -5
  16. package/dist/app/rendering/editor-layout-renderer.js +3 -2
  17. package/dist/app/rendering/editor-panels.js +27 -10
  18. package/dist/app/runtime.d.ts +1 -0
  19. package/dist/app/runtime.js +33 -14
  20. package/dist/app/session/session-event-controller.d.ts +7 -0
  21. package/dist/app/session/session-event-controller.js +78 -0
  22. package/dist/app/session/tabs-controller.js +3 -1
  23. package/dist/app/subagents/subagents-widget-controller.d.ts +10 -2
  24. package/dist/app/subagents/subagents-widget-controller.js +141 -70
  25. package/dist/app/terminal/terminal-controller.d.ts +10 -0
  26. package/dist/app/terminal/terminal-controller.js +91 -2
  27. package/dist/app/todo/todo-model.js +2 -0
  28. package/dist/app/todo/todo-widget-controller.d.ts +2 -0
  29. package/dist/app/todo/todo-widget-controller.js +17 -7
  30. package/dist/app/types.d.ts +4 -0
  31. package/dist/bundled-extensions/question/tui.js +8 -1
  32. package/dist/bundled-extensions/session-title/index.js +65 -14
  33. package/dist/input-editor-files.js +23 -4
  34. package/dist/markdown-format.d.ts +4 -1
  35. package/dist/markdown-format.js +76 -9
  36. package/external/pi-tools-suite/README.md +71 -1
  37. package/external/pi-tools-suite/package.json +3 -3
  38. package/external/pi-tools-suite/src/async-subagents/commands.ts +12 -6
  39. package/external/pi-tools-suite/src/async-subagents/index.ts +133 -37
  40. package/external/pi-tools-suite/src/context-usage.ts +6 -1
  41. package/external/pi-tools-suite/src/dcp/commands.ts +3 -2
  42. package/external/pi-tools-suite/src/dcp/compress-tool.ts +9 -4
  43. package/external/pi-tools-suite/src/dcp/config.ts +142 -6
  44. package/external/pi-tools-suite/src/dcp/index.ts +20 -8
  45. package/external/pi-tools-suite/src/dcp/prompts.ts +17 -9
  46. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +59 -15
  47. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +6 -8
  48. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +3 -3
  49. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +51 -1
  50. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +16 -11
  51. package/external/pi-tools-suite/src/model-tools/index.ts +24 -12
  52. package/external/pi-tools-suite/src/prompt-commands/index.ts +11 -2
  53. package/external/pi-tools-suite/src/telegram-mirror/index.ts +66 -27
  54. package/external/pi-tools-suite/src/todo/index.ts +87 -16
  55. package/external/pi-tools-suite/src/todo/state/store.ts +41 -10
  56. package/external/pi-tools-suite/src/todo/todo.ts +49 -6
  57. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  58. package/package.json +7 -5
@@ -10,7 +10,9 @@ export class AppSubagentsWidgetController {
10
10
  pollTimer;
11
11
  pollInFlight = false;
12
12
  currentRunDir;
13
+ currentRunDirs = [];
13
14
  state;
15
+ runFreshnessByRunDir = new Map();
14
16
  taskPreviewsByRunDir = new Map();
15
17
  snapshotByRunDir = new Map();
16
18
  refreshGeneration = 0;
@@ -38,6 +40,8 @@ export class AppSubagentsWidgetController {
38
40
  reset() {
39
41
  this.refreshGeneration++;
40
42
  this.currentRunDir = undefined;
43
+ this.currentRunDirs = [];
44
+ this.runFreshnessByRunDir.clear();
41
45
  this.taskPreviewsByRunDir.clear();
42
46
  this.snapshotByRunDir.clear();
43
47
  this.updateState(undefined);
@@ -59,15 +63,14 @@ export class AppSubagentsWidgetController {
59
63
  }
60
64
  this.currentRunDir = runDir;
61
65
  this.snapshotByRunDir.set(runDir, normalizedDetails);
66
+ this.rememberRunFreshness(runDir, this.runFreshnessFromAgents(normalizedDetails.agents) ?? Date.now());
62
67
  if (activeSubagentStates(normalizedDetails.agents).length > 0) {
63
- this.state = {
64
- runDir,
65
- agents: normalizedDetails.agents,
66
- ...(normalizedDetails.tasks === undefined ? {} : { tasks: normalizedDetails.tasks }),
68
+ this.currentRunDirs = this.orderRunDirs([runDir, ...this.currentRunDirs]);
69
+ this.updateState(this.buildStateFromRuns([normalizedDetails], {
67
70
  live: false,
68
71
  snapshotOnly: true,
69
72
  checkedAt: Date.now(),
70
- };
73
+ }));
71
74
  }
72
75
  void this.refreshFromFiles(this.refreshGeneration);
73
76
  this.schedulePoll(0);
@@ -88,6 +91,7 @@ export class AppSubagentsWidgetController {
88
91
  }))
89
92
  .filter((run) => activeSubagentStates(run.agents).length > 0);
90
93
  for (const run of activeRuns) {
94
+ this.rememberRunFreshness(run.runDir, this.runFreshnessFromAgents(run.agents) ?? data.checkedAt);
91
95
  this.snapshotByRunDir.set(run.runDir, {
92
96
  runDir: run.runDir,
93
97
  agents: run.agents,
@@ -97,22 +101,21 @@ export class AppSubagentsWidgetController {
97
101
  if (run.tasks)
98
102
  this.taskPreviewsByRunDir.set(run.runDir, run.tasks);
99
103
  }
100
- const preferred = activeRuns.find((run) => run.runDir === this.currentRunDir) ?? activeRuns[0];
101
- if (!preferred) {
104
+ if (activeRuns.length === 0) {
102
105
  this.currentRunDir = undefined;
106
+ this.currentRunDirs = [];
103
107
  this.updateState(undefined);
104
108
  this.stopPolling();
105
109
  return;
106
110
  }
107
- this.currentRunDir = preferred.runDir;
108
- this.updateState({
109
- runDir: preferred.runDir,
110
- agents: preferred.agents,
111
- ...(preferred.tasks === undefined ? {} : { tasks: preferred.tasks }),
111
+ const orderedRuns = this.orderRuns(activeRuns);
112
+ this.currentRunDir = orderedRuns[0]?.runDir;
113
+ this.currentRunDirs = orderedRuns.map((run) => run.runDir);
114
+ this.updateState(this.buildStateFromRuns(orderedRuns, {
112
115
  live: true,
113
116
  snapshotOnly: false,
114
117
  checkedAt: data.checkedAt,
115
- });
118
+ }));
116
119
  this.startFileWatcher();
117
120
  this.schedulePoll(SUBAGENTS_POLL_INTERVAL_MS);
118
121
  }
@@ -182,71 +185,77 @@ export class AppSubagentsWidgetController {
182
185
  }
183
186
  }
184
187
  async refreshFromFiles(generation = this.refreshGeneration) {
185
- let runDir = this.currentRunDir;
186
- if (!runDir) {
187
- const registry = await readSubagentRegistry(this.host.cwd);
188
- if (!this.isCurrentGeneration(generation))
189
- return;
190
- runDir = await this.findActiveRegistryRunDirForCurrentSession(registry, generation);
191
- if (!this.isCurrentGeneration(generation))
192
- return;
193
- if (runDir)
194
- this.currentRunDir = runDir;
195
- }
196
- if (!runDir) {
197
- if (!this.isCurrentGeneration(generation))
198
- return;
199
- this.updateState(undefined);
188
+ const registry = await readSubagentRegistry(this.host.cwd);
189
+ if (!this.isCurrentGeneration(generation))
200
190
  return;
201
- }
202
- this.currentRunDir = runDir;
203
- const fileState = await readSubagentRunStateFromFiles(runDir, { includeLineCounts: false });
191
+ const runDirs = await this.findActiveRegistryRunDirsForCurrentSession(registry, generation);
204
192
  if (!this.isCurrentGeneration(generation))
205
193
  return;
206
- if (!fileState) {
207
- await this.clearMissingRunAndMaybeSelectReplacement(runDir, generation);
194
+ if (runDirs.length === 0) {
195
+ this.currentRunDir = undefined;
196
+ this.currentRunDirs = [];
197
+ this.updateState(undefined);
208
198
  return;
209
199
  }
210
- const activeAgents = activeSubagentStates(fileState.agents);
211
- if (activeAgents.length === 0) {
212
- if (allSubagentStatesTerminal(fileState.agents) || fileState.agents.length === 0) {
200
+ const runs = [];
201
+ for (const runDir of runDirs) {
202
+ const fileState = await readSubagentRunStateFromFiles(runDir, { includeLineCounts: false });
203
+ if (!this.isCurrentGeneration(generation))
204
+ return;
205
+ if (!fileState) {
213
206
  this.clearCachedRun(runDir);
214
- this.currentRunDir = undefined;
215
- this.updateState(undefined);
207
+ continue;
208
+ }
209
+ const activeAgents = activeSubagentStates(fileState.agents);
210
+ if (activeAgents.length === 0) {
211
+ if (allSubagentStatesTerminal(fileState.agents) || fileState.agents.length === 0)
212
+ this.clearCachedRun(runDir);
213
+ continue;
216
214
  }
215
+ const tasks = this.taskPreviewsByRunDir.get(runDir);
216
+ runs.push({
217
+ runDir,
218
+ agents: fileState.agents,
219
+ ...(tasks === undefined ? {} : { tasks }),
220
+ });
221
+ }
222
+ if (runs.length === 0) {
223
+ this.currentRunDir = undefined;
224
+ this.currentRunDirs = [];
225
+ this.updateState(undefined);
217
226
  return;
218
227
  }
219
- const tasks = this.taskPreviewsByRunDir.get(runDir);
220
- this.updateState({
221
- runDir,
222
- agents: fileState.agents,
223
- ...(tasks === undefined ? {} : { tasks }),
228
+ const orderedRuns = this.orderRuns(runs);
229
+ this.currentRunDir = orderedRuns[0]?.runDir;
230
+ this.currentRunDirs = orderedRuns.map((run) => run.runDir);
231
+ this.updateState(this.buildStateFromRuns(orderedRuns, {
224
232
  live: true,
225
233
  snapshotOnly: false,
226
234
  checkedAt: Date.now(),
227
- });
235
+ }));
228
236
  }
229
- async findActiveRegistryRunDirForCurrentSession(registry, generation) {
237
+ async findActiveRegistryRunDirsForCurrentSession(registry, generation) {
230
238
  if (!registry)
231
- return undefined;
239
+ return [];
232
240
  const sessionFile = this.host.sessionFile();
233
241
  if (!sessionFile)
234
- return undefined;
242
+ return [];
235
243
  const candidateRunDirs = this.registryRunDirsNewestFirst(registry);
244
+ const matchingRunDirs = [];
236
245
  for (const runDir of candidateRunDirs) {
237
246
  if (!this.isCurrentGeneration(generation))
238
- return undefined;
247
+ return [];
239
248
  if (!(await subagentRunHasParentSession(runDir, sessionFile)))
240
249
  continue;
241
250
  if (!this.isCurrentGeneration(generation))
242
- return undefined;
251
+ return [];
243
252
  const state = await readSubagentRunStateFromFiles(runDir, { includeLineCounts: false });
244
253
  if (!this.isCurrentGeneration(generation))
245
- return undefined;
254
+ return [];
246
255
  if (activeSubagentStates(state?.agents ?? []).length > 0)
247
- return runDir;
256
+ matchingRunDirs.push(runDir);
248
257
  }
249
- return undefined;
258
+ return matchingRunDirs;
250
259
  }
251
260
  registryRunDirsNewestFirst(registry) {
252
261
  const runDirs = [];
@@ -263,30 +272,89 @@ export class AppSubagentsWidgetController {
263
272
  addRunDir(registry.latestRunDir);
264
273
  Object.values(registry.runs)
265
274
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
266
- .forEach((run) => addRunDir(run.runDir));
275
+ .forEach((run) => {
276
+ this.rememberRunFreshness(run.runDir, this.parseRunTimestamp(run.updatedAt) ?? this.parseRunTimestamp(run.createdAt));
277
+ addRunDir(run.runDir);
278
+ });
267
279
  return runDirs;
268
280
  }
269
- async clearMissingRunAndMaybeSelectReplacement(runDir, generation) {
270
- this.clearCachedRun(runDir);
271
- if (this.currentRunDir === runDir)
272
- this.currentRunDir = undefined;
273
- const registry = await readSubagentRegistry(this.host.cwd);
274
- if (!this.isCurrentGeneration(generation))
275
- return;
276
- const replacementRunDir = await this.findActiveRegistryRunDirForCurrentSession(registry, generation);
277
- if (!this.isCurrentGeneration(generation))
278
- return;
279
- if (replacementRunDir) {
280
- this.currentRunDir = replacementRunDir;
281
- await this.refreshFromFiles(generation);
282
- return;
283
- }
284
- this.updateState(undefined);
285
- }
286
281
  clearCachedRun(runDir) {
282
+ this.runFreshnessByRunDir.delete(runDir);
287
283
  this.snapshotByRunDir.delete(runDir);
288
284
  this.taskPreviewsByRunDir.delete(runDir);
289
285
  }
286
+ buildStateFromRuns(runs, meta) {
287
+ const activeRuns = runs
288
+ .map((run) => ({ ...run, agents: activeSubagentStates(run.agents) }))
289
+ .filter((run) => run.agents.length > 0);
290
+ if (activeRuns.length === 0)
291
+ return undefined;
292
+ const primary = activeRuns[0];
293
+ if (!primary)
294
+ return undefined;
295
+ return {
296
+ runs: activeRuns.map((run) => ({
297
+ runDir: run.runDir,
298
+ agents: run.agents,
299
+ ...(run.tasks === undefined ? {} : { tasks: run.tasks }),
300
+ })),
301
+ runDir: primary.runDir,
302
+ agents: activeRuns.flatMap((run) => run.agents),
303
+ ...(primary.tasks === undefined ? {} : { tasks: primary.tasks }),
304
+ ...meta,
305
+ };
306
+ }
307
+ orderRuns(runs) {
308
+ const order = this.orderRunDirs(runs.map((run) => run.runDir));
309
+ const priority = new Map(order.map((runDir, index) => [runDir, index]));
310
+ return [...runs].sort((a, b) => {
311
+ const freshness = this.runFreshness(b) - this.runFreshness(a);
312
+ if (freshness !== 0)
313
+ return freshness;
314
+ return (priority.get(a.runDir) ?? Number.MAX_SAFE_INTEGER) - (priority.get(b.runDir) ?? Number.MAX_SAFE_INTEGER);
315
+ });
316
+ }
317
+ orderRunDirs(runDirs) {
318
+ const seen = new Set();
319
+ const preferred = [this.currentRunDir, ...this.currentRunDirs, ...runDirs].filter((runDir) => Boolean(runDir));
320
+ const ordered = [];
321
+ for (const runDir of preferred) {
322
+ if (seen.has(runDir))
323
+ continue;
324
+ seen.add(runDir);
325
+ ordered.push(runDir);
326
+ }
327
+ return ordered;
328
+ }
329
+ rememberRunFreshness(runDir, freshness) {
330
+ if (!Number.isFinite(freshness))
331
+ return;
332
+ const next = freshness ?? 0;
333
+ const previous = this.runFreshnessByRunDir.get(runDir) ?? Number.NEGATIVE_INFINITY;
334
+ if (next >= previous)
335
+ this.runFreshnessByRunDir.set(runDir, next);
336
+ }
337
+ runFreshness(run) {
338
+ return this.runFreshnessFromAgents(run.agents)
339
+ ?? this.runFreshnessByRunDir.get(run.runDir)
340
+ ?? 0;
341
+ }
342
+ runFreshnessFromAgents(agents) {
343
+ let freshest;
344
+ for (const agent of agents) {
345
+ const parsed = this.parseRunTimestamp(agent.startedAt);
346
+ if (parsed === undefined)
347
+ continue;
348
+ freshest = freshest === undefined ? parsed : Math.max(freshest, parsed);
349
+ }
350
+ return freshest;
351
+ }
352
+ parseRunTimestamp(value) {
353
+ if (!value)
354
+ return undefined;
355
+ const parsed = Date.parse(value);
356
+ return Number.isFinite(parsed) ? parsed : undefined;
357
+ }
290
358
  updateState(next) {
291
359
  const previous = stringifyUnknown(this.state);
292
360
  const serializedNext = stringifyUnknown(next);
@@ -298,6 +366,9 @@ export class AppSubagentsWidgetController {
298
366
  return generation === this.refreshGeneration;
299
367
  }
300
368
  shouldContinuePolling() {
369
+ const runs = this.state?.runs;
370
+ if (runs?.length)
371
+ return runs.some((run) => activeSubagentStates(run.agents).length > 0);
301
372
  return activeSubagentStates(this.state?.agents ?? []).length > 0;
302
373
  }
303
374
  eventMatchesCurrentSession(eventSessionFile) {
@@ -24,6 +24,10 @@ export declare class AppTerminalController {
24
24
  private terminalEnabled;
25
25
  private interactiveSuspended;
26
26
  private stopPromise;
27
+ private keyboardProtocolNegotiationBuffer;
28
+ private keyboardProtocolBufferFlushTimer;
29
+ private kittyProtocolActive;
30
+ private modifyOtherKeysActive;
27
31
  private readonly enterInteractiveSequence;
28
32
  private readonly exitInteractiveSequence;
29
33
  constructor(host: AppTerminalControllerHost);
@@ -40,4 +44,10 @@ export declare class AppTerminalController {
40
44
  private scheduleForcedProcessExit;
41
45
  private readonly onResize;
42
46
  private readonly onInputData;
47
+ private beginKeyboardProtocolNegotiation;
48
+ private filterKeyboardProtocolNegotiationInput;
49
+ private handleKeyboardProtocolNegotiationResponse;
50
+ private enableModifyOtherKeysFallback;
51
+ private setKeyboardProtocolNegotiationBuffer;
52
+ private clearKeyboardProtocolNegotiationBuffer;
43
53
  }
@@ -1,10 +1,14 @@
1
1
  import { ANSI_RESET } from "../../theme.js";
2
- import { DISABLE_BRACKETED_PASTE, DISABLE_TERMINAL_KEY_REPORTING, DISABLE_TERMINAL_WRAP, CLEAR_TERMINAL, ENABLE_BRACKETED_PASTE, ENABLE_TERMINAL_KEY_REPORTING, ENABLE_TERMINAL_WRAP, HIDE_CURSOR, RESET_TERMINAL_VIEWPORT_STATE, RUNTIME_DISPOSE_GRACE_MS, SHOW_CURSOR, } from "../constants.js";
2
+ import { DISABLE_BRACKETED_PASTE, DISABLE_TERMINAL_KEY_REPORTING, DISABLE_TERMINAL_WRAP, CLEAR_TERMINAL, ENABLE_BRACKETED_PASTE, ENABLE_TERMINAL_MODIFY_OTHER_KEYS, ENABLE_TERMINAL_KEY_REPORTING, ENABLE_TERMINAL_WRAP, HIDE_CURSOR, RESET_TERMINAL_VIEWPORT_STATE, RUNTIME_DISPOSE_GRACE_MS, SHOW_CURSOR, } from "../constants.js";
3
3
  export class AppTerminalController {
4
4
  host;
5
5
  terminalEnabled = false;
6
6
  interactiveSuspended = false;
7
7
  stopPromise;
8
+ keyboardProtocolNegotiationBuffer = "";
9
+ keyboardProtocolBufferFlushTimer;
10
+ kittyProtocolActive = false;
11
+ modifyOtherKeysActive = false;
8
12
  enterInteractiveSequence = `${ANSI_RESET}${RESET_TERMINAL_VIEWPORT_STATE}${CLEAR_TERMINAL}\x1b[?1049h${RESET_TERMINAL_VIEWPORT_STATE}${CLEAR_TERMINAL}${ENABLE_TERMINAL_KEY_REPORTING}${ENABLE_BRACKETED_PASTE}${DISABLE_TERMINAL_WRAP}\x1b[?1002h\x1b[?1006h${HIDE_CURSOR}`;
9
13
  exitInteractiveSequence = `${ANSI_RESET}${RESET_TERMINAL_VIEWPORT_STATE}${DISABLE_TERMINAL_KEY_REPORTING}${DISABLE_BRACKETED_PASTE}${ENABLE_TERMINAL_WRAP}\x1b[?1006l\x1b[?1002l\x1b[?1049l${SHOW_CURSOR}`;
10
14
  constructor(host) {
@@ -17,6 +21,7 @@ export class AppTerminalController {
17
21
  if (this.terminalEnabled)
18
22
  return;
19
23
  this.terminalEnabled = true;
24
+ this.beginKeyboardProtocolNegotiation();
20
25
  process.stdin.setRawMode(true);
21
26
  process.stdin.resume();
22
27
  process.stdin.on("data", this.onInputData);
@@ -66,6 +71,7 @@ export class AppTerminalController {
66
71
  restoreTerminal = () => {
67
72
  if (!this.terminalEnabled)
68
73
  return;
74
+ this.clearKeyboardProtocolNegotiationBuffer();
69
75
  this.terminalEnabled = false;
70
76
  this.interactiveSuspended = false;
71
77
  process.stdout.write(this.exitInteractiveSequence);
@@ -87,6 +93,7 @@ export class AppTerminalController {
87
93
  if (!this.terminalEnabled || !this.interactiveSuspended)
88
94
  return;
89
95
  this.interactiveSuspended = false;
96
+ this.beginKeyboardProtocolNegotiation();
90
97
  if (process.stdin.isTTY)
91
98
  process.stdin.setRawMode(true);
92
99
  process.stdin.resume();
@@ -99,6 +106,7 @@ export class AppTerminalController {
99
106
  async stopInternal() {
100
107
  if (!this.host.isRunning())
101
108
  return;
109
+ this.clearKeyboardProtocolNegotiationBuffer();
102
110
  this.host.setRunning(false);
103
111
  this.host.closeSdkMenuForStop();
104
112
  this.host.clearToastTimers();
@@ -133,6 +141,87 @@ export class AppTerminalController {
133
141
  this.host.render();
134
142
  };
135
143
  onInputData = (chunk) => {
136
- this.host.handleInputChunk(chunk);
144
+ const input = this.filterKeyboardProtocolNegotiationInput(chunk.toString("utf8"));
145
+ if (input)
146
+ this.host.handleInputChunk(Buffer.from(input, "utf8"));
137
147
  };
148
+ beginKeyboardProtocolNegotiation() {
149
+ this.clearKeyboardProtocolNegotiationBuffer();
150
+ this.kittyProtocolActive = false;
151
+ this.modifyOtherKeysActive = false;
152
+ }
153
+ filterKeyboardProtocolNegotiationInput(data) {
154
+ let input = this.keyboardProtocolNegotiationBuffer + data;
155
+ this.clearKeyboardProtocolNegotiationBuffer();
156
+ let output = "";
157
+ while (input.length > 0) {
158
+ const response = readKeyboardProtocolNegotiationResponse(input);
159
+ if (response.kind === "complete") {
160
+ this.handleKeyboardProtocolNegotiationResponse(response.response);
161
+ input = input.slice(response.length);
162
+ continue;
163
+ }
164
+ if (response.kind === "pending") {
165
+ this.setKeyboardProtocolNegotiationBuffer(input);
166
+ break;
167
+ }
168
+ output += input[0];
169
+ input = input.slice(1);
170
+ }
171
+ return output;
172
+ }
173
+ handleKeyboardProtocolNegotiationResponse(response) {
174
+ if (response.type === "kitty-flags") {
175
+ if (response.flags !== 0) {
176
+ this.kittyProtocolActive = true;
177
+ this.modifyOtherKeysActive = false;
178
+ }
179
+ else {
180
+ this.enableModifyOtherKeysFallback();
181
+ }
182
+ return;
183
+ }
184
+ if (!this.kittyProtocolActive)
185
+ this.enableModifyOtherKeysFallback();
186
+ }
187
+ enableModifyOtherKeysFallback() {
188
+ if (this.kittyProtocolActive || this.modifyOtherKeysActive)
189
+ return;
190
+ process.stdout.write(ENABLE_TERMINAL_MODIFY_OTHER_KEYS);
191
+ this.modifyOtherKeysActive = true;
192
+ }
193
+ setKeyboardProtocolNegotiationBuffer(data) {
194
+ this.clearKeyboardProtocolNegotiationBuffer();
195
+ this.keyboardProtocolNegotiationBuffer = data;
196
+ this.keyboardProtocolBufferFlushTimer = setTimeout(() => {
197
+ this.keyboardProtocolBufferFlushTimer = undefined;
198
+ const buffered = this.keyboardProtocolNegotiationBuffer;
199
+ this.keyboardProtocolNegotiationBuffer = "";
200
+ if (buffered)
201
+ this.host.handleInputChunk(Buffer.from(buffered, "utf8"));
202
+ }, 150);
203
+ }
204
+ clearKeyboardProtocolNegotiationBuffer() {
205
+ if (this.keyboardProtocolBufferFlushTimer)
206
+ clearTimeout(this.keyboardProtocolBufferFlushTimer);
207
+ this.keyboardProtocolBufferFlushTimer = undefined;
208
+ this.keyboardProtocolNegotiationBuffer = "";
209
+ }
210
+ }
211
+ function readKeyboardProtocolNegotiationResponse(input) {
212
+ const kittyFlags = /^\x1b\[\?(\d+)u/.exec(input);
213
+ if (kittyFlags) {
214
+ return {
215
+ kind: "complete",
216
+ response: { type: "kitty-flags", flags: Number.parseInt(kittyFlags[1] ?? "0", 10) },
217
+ length: kittyFlags[0].length,
218
+ };
219
+ }
220
+ const deviceAttributes = /^\x1b\[\?[\d;]*c/.exec(input);
221
+ if (deviceAttributes) {
222
+ return { kind: "complete", response: { type: "device-attributes" }, length: deviceAttributes[0].length };
223
+ }
224
+ if (input === "\x1b[" || /^\x1b\[\?[\d;]*$/.test(input))
225
+ return { kind: "pending" };
226
+ return { kind: "none" };
138
227
  }
@@ -59,6 +59,8 @@ export function isTodoLiveStateEvent(value) {
59
59
  return false;
60
60
  if (value.sessionFile !== undefined && typeof value.sessionFile !== "string")
61
61
  return false;
62
+ if (value.sessionId !== undefined && typeof value.sessionId !== "string")
63
+ return false;
62
64
  return typeof value.checkedAt === "number" && Number.isFinite(value.checkedAt);
63
65
  }
64
66
  export function todoStatusIcon(status) {
@@ -1,6 +1,7 @@
1
1
  import type { TodoDetails } from "../types.js";
2
2
  export type TodoWidgetControllerHost = {
3
3
  sessionFile?(): string | undefined;
4
+ sessionId?(): string | undefined;
4
5
  isRunning(): boolean;
5
6
  render(): void;
6
7
  };
@@ -17,5 +18,6 @@ export declare class AppTodoWidgetController {
17
18
  private updateDetailsForCurrentSession;
18
19
  private updateDetailsForSession;
19
20
  private currentSessionDetails;
21
+ private currentSessionKey;
20
22
  private sessionKey;
21
23
  }
@@ -25,17 +25,21 @@ export class AppTodoWidgetController {
25
25
  observeLiveState(data) {
26
26
  if (!isTodoLiveStateEvent(data))
27
27
  return;
28
- this.updateDetailsForSession(data.sessionFile ?? this.host.sessionFile?.(), this.visibleDetails(data.details));
28
+ const sessionFile = data.sessionFile;
29
+ const sessionId = data.sessionId;
30
+ if (!sessionFile && !sessionId && this.currentSessionKey())
31
+ return;
32
+ this.updateDetailsForSession(sessionFile, sessionId, this.visibleDetails(data.details));
29
33
  }
30
34
  visibleDetails(details) {
31
35
  return hasOpenTodoTasks(details) ? details : undefined;
32
36
  }
33
37
  updateDetailsForCurrentSession(next, options = {}) {
34
- this.updateDetailsForSession(this.host.sessionFile?.(), next, options);
38
+ this.updateDetailsForSession(this.host.sessionFile?.(), this.host.sessionId?.(), next, options);
35
39
  }
36
- updateDetailsForSession(sessionFile, next, options = {}) {
40
+ updateDetailsForSession(sessionFile, sessionId, next, options = {}) {
37
41
  const previous = stringifyUnknown(this.currentSessionDetails());
38
- const key = this.sessionKey(sessionFile);
42
+ const key = this.sessionKey(sessionFile, sessionId);
39
43
  if (key) {
40
44
  if (next)
41
45
  this.detailsBySessionFile.set(key, next);
@@ -50,10 +54,16 @@ export class AppTodoWidgetController {
50
54
  this.host.render();
51
55
  }
52
56
  currentSessionDetails() {
53
- const key = this.sessionKey(this.host.sessionFile?.());
57
+ const key = this.currentSessionKey();
54
58
  return key ? this.detailsBySessionFile.get(key) : this.unscopedDetails;
55
59
  }
56
- sessionKey(sessionFile) {
57
- return sessionFile ? resolve(sessionFile) : undefined;
60
+ currentSessionKey() {
61
+ return this.sessionKey(this.host.sessionFile?.(), this.host.sessionId?.());
62
+ }
63
+ sessionKey(sessionFile, sessionId) {
64
+ if (sessionFile)
65
+ return `file:${resolve(sessionFile)}`;
66
+ const normalizedSessionId = sessionId?.trim();
67
+ return normalizedSessionId ? `id:${normalizedSessionId}` : undefined;
58
68
  }
59
69
  }
@@ -74,6 +74,8 @@ export type Entry = {
74
74
  expanded: boolean;
75
75
  status: "running" | "done";
76
76
  level?: string;
77
+ startedAt?: number;
78
+ finishedAt?: number;
77
79
  } | {
78
80
  id: string;
79
81
  kind: "tool";
@@ -125,6 +127,7 @@ export type TodoLiveStateEvent = {
125
127
  version: 1;
126
128
  details: TodoDetails;
127
129
  sessionFile?: string;
130
+ sessionId?: string;
128
131
  checkedAt: number;
129
132
  };
130
133
  export type SubagentStatus = (typeof SUBAGENT_STATUSES)[number];
@@ -193,6 +196,7 @@ export type SubagentRegistryAgent = {
193
196
  updatedAt: string;
194
197
  };
195
198
  export type SubagentsWidgetState = {
199
+ runs?: SubagentLiveStateRun[];
196
200
  runDir: string;
197
201
  agents: SubagentAgentState[];
198
202
  tasks?: SubagentTaskPreview[];
@@ -60,6 +60,12 @@ function wrapLine(line, width) {
60
60
  lines.push(current.trimEnd());
61
61
  return lines.length > 0 ? lines : [""];
62
62
  }
63
+ function wrapIndentedLine(line, width) {
64
+ const indent = line.match(/^\s*/)?.[0] ?? "";
65
+ const content = line.slice(indent.length);
66
+ const contentWidth = Math.max(1, width - indent.length);
67
+ return wrapLine(content, contentWidth).map((wrapped) => `${indent}${wrapped}`);
68
+ }
63
69
  function stripAnsi(text) {
64
70
  return text.replace(/\u001b\[[0-9;]*m/g, "");
65
71
  }
@@ -191,7 +197,8 @@ export async function runQuestionnaire(questions, ctx) {
191
197
  clickZones.push({ ...zone, row, startColumn: 1, endColumn: width + 1 });
192
198
  }
193
199
  function renderMutedLine(add, text, width) {
194
- add(paintLine(theme, text, width, { foreground: "muted" }));
200
+ for (const line of wrapIndentedLine(text, width))
201
+ add(paintLine(theme, line, width, { foreground: "muted" }));
195
202
  }
196
203
  function moveToQuestion(index) {
197
204
  captureCustomDraft();