pi-daddy 0.46.1 → 0.47.0

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 (106) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +41 -11
  3. package/contracts/ledger-record/v1/governance-event.schema.json +65 -3
  4. package/dist/governance/approval-prompt.d.ts +4 -0
  5. package/dist/governance/approval-prompt.d.ts.map +1 -1
  6. package/dist/governance/approval-prompt.js +59 -4
  7. package/dist/governance/approval-prompt.js.map +1 -1
  8. package/dist/governance/auto-mode-policy.d.ts +11 -0
  9. package/dist/governance/auto-mode-policy.d.ts.map +1 -0
  10. package/dist/governance/auto-mode-policy.js +309 -0
  11. package/dist/governance/auto-mode-policy.js.map +1 -0
  12. package/dist/governance/execution-retention.d.ts +3 -0
  13. package/dist/governance/execution-retention.d.ts.map +1 -1
  14. package/dist/governance/execution-retention.js +30 -0
  15. package/dist/governance/execution-retention.js.map +1 -1
  16. package/dist/governance/ledger-events.d.ts +14 -1
  17. package/dist/governance/ledger-events.d.ts.map +1 -1
  18. package/dist/governance/ledger-events.js +13 -0
  19. package/dist/governance/ledger-events.js.map +1 -1
  20. package/dist/governance/ledger-report.d.ts.map +1 -1
  21. package/dist/governance/ledger-report.js +5 -3
  22. package/dist/governance/ledger-report.js.map +1 -1
  23. package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
  24. package/dist/governance/ledger-v3-validation.js +17 -3
  25. package/dist/governance/ledger-v3-validation.js.map +1 -1
  26. package/dist/governance/ledger.d.ts +1 -1
  27. package/dist/governance/ledger.d.ts.map +1 -1
  28. package/dist/governance/ledger.js +1 -1
  29. package/dist/governance/ledger.js.map +1 -1
  30. package/dist/kernel/approval.d.ts +1 -1
  31. package/dist/kernel/approval.d.ts.map +1 -1
  32. package/dist/kernel/approval.js +1 -1
  33. package/dist/kernel/approval.js.map +1 -1
  34. package/dist/kernel/auto-mode.d.ts +35 -0
  35. package/dist/kernel/auto-mode.d.ts.map +1 -0
  36. package/dist/kernel/auto-mode.js +29 -0
  37. package/dist/kernel/auto-mode.js.map +1 -0
  38. package/dist/kernel/delegate-types.d.ts +2 -0
  39. package/dist/kernel/delegate-types.d.ts.map +1 -1
  40. package/dist/kernel/delegate-types.js.map +1 -1
  41. package/dist/kernel/delegate.d.ts.map +1 -1
  42. package/dist/kernel/delegate.js +2 -2
  43. package/dist/kernel/delegate.js.map +1 -1
  44. package/dist/kernel/env-names.d.ts +2 -0
  45. package/dist/kernel/env-names.d.ts.map +1 -1
  46. package/dist/kernel/env-names.js +4 -0
  47. package/dist/kernel/env-names.js.map +1 -1
  48. package/dist/kernel/propagation.d.ts +5 -1
  49. package/dist/kernel/propagation.d.ts.map +1 -1
  50. package/dist/kernel/propagation.js +8 -2
  51. package/dist/kernel/propagation.js.map +1 -1
  52. package/dist/products/dashboard-cli.d.ts +6 -0
  53. package/dist/products/dashboard-cli.d.ts.map +1 -1
  54. package/dist/products/dashboard-cli.js +248 -52
  55. package/dist/products/dashboard-cli.js.map +1 -1
  56. package/dist/products/dashboard-display-controls.d.ts +2 -1
  57. package/dist/products/dashboard-display-controls.d.ts.map +1 -1
  58. package/dist/products/dashboard-display-controls.js +61 -0
  59. package/dist/products/dashboard-display-controls.js.map +1 -1
  60. package/dist/products/dashboard-render.d.ts +2 -0
  61. package/dist/products/dashboard-render.d.ts.map +1 -1
  62. package/dist/products/dashboard-render.js +2 -2
  63. package/dist/products/dashboard-render.js.map +1 -1
  64. package/dist/products/dashboard-screen.d.ts +52 -0
  65. package/dist/products/dashboard-screen.d.ts.map +1 -0
  66. package/dist/products/dashboard-screen.js +262 -0
  67. package/dist/products/dashboard-screen.js.map +1 -0
  68. package/dist/products/dashboard-session-client.d.ts +34 -2
  69. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  70. package/dist/products/dashboard-session-client.js +51 -0
  71. package/dist/products/dashboard-session-client.js.map +1 -1
  72. package/dist/products/ecosystem-versions.d.ts +19 -0
  73. package/dist/products/ecosystem-versions.d.ts.map +1 -0
  74. package/dist/products/ecosystem-versions.js +2 -0
  75. package/dist/products/ecosystem-versions.js.map +1 -0
  76. package/extensions/approvals.ts +3 -2
  77. package/extensions/dashboard-session-server.ts +32 -4
  78. package/extensions/delegate-chain.ts +2 -0
  79. package/extensions/delegation-ledger.ts +10 -6
  80. package/extensions/ecosystem-versions.ts +166 -0
  81. package/extensions/grants-command.ts +33 -3
  82. package/extensions/grants.ts +28 -2
  83. package/extensions/reload-environment.ts +16 -1
  84. package/extensions/run-delegation.ts +49 -3
  85. package/extensions/session-auto-mode.ts +132 -0
  86. package/extensions/session.ts +47 -37
  87. package/package.json +2 -2
  88. package/src/governance/approval-prompt.ts +62 -4
  89. package/src/governance/auto-mode-policy.ts +297 -0
  90. package/src/governance/execution-retention.ts +24 -0
  91. package/src/governance/ledger-events.ts +22 -1
  92. package/src/governance/ledger-report.ts +5 -3
  93. package/src/governance/ledger-v3-validation.ts +17 -3
  94. package/src/governance/ledger.ts +1 -0
  95. package/src/kernel/approval.ts +1 -1
  96. package/src/kernel/auto-mode.ts +56 -0
  97. package/src/kernel/delegate-types.ts +2 -0
  98. package/src/kernel/delegate.ts +2 -1
  99. package/src/kernel/env-names.ts +4 -0
  100. package/src/kernel/propagation.ts +11 -2
  101. package/src/products/dashboard-cli.ts +255 -53
  102. package/src/products/dashboard-display-controls.ts +52 -0
  103. package/src/products/dashboard-render.ts +2 -2
  104. package/src/products/dashboard-screen.ts +339 -0
  105. package/src/products/dashboard-session-client.ts +60 -1
  106. package/src/products/ecosystem-versions.ts +18 -0
@@ -9,11 +9,12 @@ import { realpathSync } from "node:fs";
9
9
  import { readFile } from "node:fs/promises";
10
10
  import { resolve } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
- import { createInterface } from "node:readline";
12
+ import { createInterface, emitKeypressEvents, type Key } from "node:readline";
13
13
  import { parseDashboardLedger } from "./dashboard-projection.ts";
14
14
  import { renderDashboard } from "./dashboard-render.ts";
15
15
  import {
16
16
  ActivityTimelineAliases,
17
+ activityTaskKey,
17
18
  defaultActivityTimelinePath,
18
19
  detailForTimeline,
19
20
  parseActivityTimeline,
@@ -22,9 +23,20 @@ import {
22
23
  type TimelineFilter,
23
24
  } from "./activity-timeline.ts";
24
25
  import { createDashboardDisplayControls } from "./dashboard-display-controls.ts";
25
- import { dashboardSessionRequest, type DashboardSessionSnapshot } from "./dashboard-session-client.ts";
26
+ import {
27
+ createDashboardConnection,
28
+ dashboardSessionRequest,
29
+ type DashboardSessionSnapshot,
30
+ } from "./dashboard-session-client.ts";
26
31
  import { adoptLegacyEnvironment, ENV_LEDGER, legacyEnvironmentWarning } from "../kernel/env-names.ts";
27
32
  import { projectLedgerPath } from "../kernel/project-paths.ts";
33
+ import {
34
+ activityDashboardItems,
35
+ ledgerDashboardItems,
36
+ renderDashboardScreen,
37
+ type DashboardItem,
38
+ type DashboardScreenState,
39
+ } from "./dashboard-screen.ts";
28
40
 
29
41
  export const DASHBOARD_PROTOCOL_VERSION = 1 as const;
30
42
  export const ENV_DASHBOARD_LEDGER = ENV_LEDGER; // one ledger path variable for children and the dashboard (ADR-0076 PR 3b)
@@ -47,6 +59,11 @@ export interface DashboardFrameOptions {
47
59
  activityAliases?: ActivityTimelineAliases;
48
60
  now?: Date;
49
61
  session?: DashboardSessionSnapshot;
62
+ height?: number;
63
+ screen?: DashboardScreenState;
64
+ notice?: string;
65
+ pending?: boolean;
66
+ command?: string;
50
67
  }
51
68
 
52
69
  function shellQuote(value: string): string {
@@ -102,7 +119,106 @@ async function renderTimelineFile(path: string, text: string, options: Dashboard
102
119
  });
103
120
  }
104
121
 
122
+ function isActivityTimelineText(text: string): boolean {
123
+ try {
124
+ const first = JSON.parse(text.trimStart().split("\n")[0] || "{}");
125
+ return first?.kind === "activity" || first?.version === 1;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ async function compactFrame(options: DashboardFrameOptions): Promise<string> {
132
+ const path = resolve(options.cwd, options.ledgerPath ?? defaultActivityTimelinePath(options.cwd));
133
+ let items: DashboardItem[] = [],
134
+ problem: string | undefined,
135
+ current: string | undefined,
136
+ currentStatus: string | undefined;
137
+ let content: { title: string; text: string } | undefined;
138
+ const aliases = options.activityAliases ?? new ActivityTimelineAliases();
139
+ if (options.protocol !== undefined && options.protocol !== DASHBOARD_PROTOCOL_VERSION)
140
+ return renderDashboardScreen({
141
+ ...options,
142
+ width: options.width ?? 80,
143
+ height: options.height!,
144
+ screen: options.screen!,
145
+ items: [],
146
+ problem: "INCOMPATIBLE dashboard protocol; relink this package.",
147
+ });
148
+ try {
149
+ const text = await readFile(path, "utf8");
150
+ if (!options.ledgerPath || isActivityTimelineText(text)) {
151
+ const timeline = parseActivityTimeline(text);
152
+ items = activityDashboardItems(timeline, aliases, options.now ?? new Date()).map((item) => ({
153
+ ...item,
154
+ timelinePath: path,
155
+ }));
156
+ problem = timeline.refusals[0];
157
+ } else {
158
+ const projection = parseDashboardLedger(text, { now: options.now });
159
+ items = ledgerDashboardItems(projection);
160
+ if (projection.corrupt.length) problem = `Ledger unavailable: ${projection.corrupt.length} corrupt line(s)`;
161
+ }
162
+ } catch (error) {
163
+ problem =
164
+ (error as { code?: string }).code === "ENOENT"
165
+ ? "Waiting for local work; open from /grants dashboard."
166
+ : `Evidence unavailable: ${error instanceof Error ? error.message : String(error)}`;
167
+ }
168
+ // The owner supplies this exact current turn. No sibling timeline or unrelated history is merged.
169
+ const activity = options.session?.activity;
170
+ if (activity) {
171
+ const key = activityTaskKey(activity.rootId, activity.taskId);
172
+ current = `Task ${activity.taskId}`;
173
+ try {
174
+ const timeline = parseActivityTimeline(await readFile(activity.path, "utf8"));
175
+ timeline.tasks = timeline.tasks.filter((task) => activityTaskKey(task.rootId, task.id) === key);
176
+ const item = activityDashboardItems(timeline, aliases, options.now ?? new Date())[0];
177
+ if (item) {
178
+ current = item.label;
179
+ currentStatus = item.status;
180
+ if (!items.some((existing) => existing.key === key)) items.unshift({ ...item, timelinePath: activity.path });
181
+ try {
182
+ current = (await detailForTimeline(activity.path, key, "prompt")).text.slice(0, 600);
183
+ } catch {
184
+ /* Metadata-only/off and unavailable retained content retain the exact task identity. */
185
+ }
186
+ }
187
+ } catch {
188
+ /* The owner identity remains useful when its local timeline is unavailable. */
189
+ }
190
+ }
191
+ if (options.activityDetail) {
192
+ try {
193
+ const requested = aliases.resolve(options.activityDetail.taskKey) ?? options.activityDetail.taskKey;
194
+ const key = items.find((value) => value.taskKey === requested)?.taskKey;
195
+ if (!key) throw Error("unknown task selector");
196
+ options.screen!.selectedKey = key;
197
+ const source = items.find((value) => value.taskKey === key)?.timelinePath;
198
+ if (!source) throw Error("task has no retained activity source");
199
+ content = {
200
+ title: `PRIVATE ${options.activityDetail.field.toUpperCase()}`,
201
+ text: (await detailForTimeline(source, key, options.activityDetail.field)).text,
202
+ };
203
+ } catch (error) {
204
+ problem = `Activity detail unavailable: ${error instanceof Error ? error.message : String(error)}`;
205
+ }
206
+ }
207
+ return renderDashboardScreen({
208
+ ...options,
209
+ width: options.width ?? 80,
210
+ height: options.height!,
211
+ screen: options.screen!,
212
+ items,
213
+ problem,
214
+ content,
215
+ current,
216
+ currentStatus,
217
+ });
218
+ }
219
+
105
220
  export async function dashboardFrame(options: DashboardFrameOptions): Promise<string> {
221
+ if (options.screen && options.height !== undefined) return compactFrame(options);
106
222
  if (options.protocol !== undefined && options.protocol !== DASHBOARD_PROTOCOL_VERSION) {
107
223
  return incompatibleFrame(options.protocol);
108
224
  }
@@ -136,7 +252,7 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
136
252
  ].join("\n");
137
253
  }
138
254
  }
139
- const rendered = text.trimStart().startsWith('{"version":1')
255
+ const rendered = isActivityTimelineText(text)
140
256
  ? await renderTimelineFile(ledgerPath, text, options)
141
257
  : renderDashboard(parseDashboardLedger(text, { now: options.now }), {
142
258
  color: options.color,
@@ -186,17 +302,21 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
186
302
  process.title = `pi-daddy-dashboard${key ? `:${key.slice(0, 12)}` : ""}`;
187
303
 
188
304
  const display = createDashboardDisplayControls(cli.details, true);
189
- let previous = "";
190
- let notice = "";
305
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !cli.once;
306
+ const connection = createDashboardConnection(
307
+ sessionSocket && sessionToken
308
+ ? (action) => dashboardSessionRequest(sessionSocket, sessionToken, action)
309
+ : undefined,
310
+ );
311
+ let previous = "",
312
+ notice = "",
313
+ command: string | undefined;
191
314
  let input: ReturnType<typeof createInterface> | null = null;
315
+ let stopped = false,
316
+ drawing = false;
192
317
  const draw = async (clear: boolean): Promise<void> => {
193
- let session: DashboardSessionSnapshot | undefined;
194
- if (sessionSocket && sessionToken)
195
- try {
196
- session = await dashboardSessionRequest(sessionSocket, sessionToken, { action: "get" });
197
- } catch (error) {
198
- notice ||= `session controls unavailable: ${error instanceof Error ? error.message : String(error)}`;
199
- }
318
+ await connection.refresh();
319
+ if (stopped) return;
200
320
  const view = await dashboardFrame({
201
321
  cwd,
202
322
  ledgerPath,
@@ -205,62 +325,144 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
205
325
  width: process.stdout.columns || 80,
206
326
  ...display.state,
207
327
  activityAliases: display.aliases,
208
- session,
328
+ session: connection.state.snapshot,
329
+ ...(interactive
330
+ ? {
331
+ height: process.stdout.rows || 24,
332
+ screen: display.screen,
333
+ notice: connection.state.error || notice,
334
+ pending: connection.state.pending,
335
+ command,
336
+ }
337
+ : {}),
209
338
  });
210
- const frame = notice ? `${notice}\n\n${view}` : view;
211
- if (frame === previous && !clear) return;
212
- previous = frame;
213
- const prompt = display.prompt();
214
- const draft = input?.line ?? "";
339
+ if (stopped) return; // A read may complete after q/SIGTERM restored the parent terminal.
340
+ const frame = interactive
341
+ ? view
342
+ : `${connection.state.error || notice ? `${connection.state.error || notice}\n\n` : ""}${view}`;
343
+ const prompt = display.prompt(),
344
+ draft = input?.line ?? "";
345
+ const rendered = `${frame}${!interactive && input ? `\n\n${prompt}${draft}` : ""}`;
346
+ if (rendered === previous) return;
347
+ previous = rendered;
215
348
  process.stdout.write(
216
- clear ? `\u001b]0;PI-DADDY\u0007\u001b[2J\u001b[H${frame}\n\n${input ? prompt + draft : ""}` : `${frame}\n`,
349
+ interactive
350
+ ? `\u001b[H${frame}\u001b[J`
351
+ : clear
352
+ ? `\u001b]0;PI-DADDY\u0007\u001b[2J\u001b[H${rendered}`
353
+ : `${rendered}\n`,
217
354
  );
218
- if (input) input.setPrompt(prompt);
355
+ input?.setPrompt(prompt);
219
356
  };
220
-
221
357
  if (cli.once) {
222
358
  await draw(false);
223
359
  return;
224
360
  }
225
- input = createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
226
- await draw(true);
227
- let drawing = false;
228
361
  const redraw = (): void => {
229
- if (drawing) return;
362
+ if (drawing || stopped) return;
230
363
  drawing = true;
231
- void draw(true).finally(() => {
232
- drawing = false;
233
- });
364
+ void draw(true)
365
+ .catch((error) => {
366
+ notice = `Dashboard unavailable: ${String(error)}`;
367
+ })
368
+ .finally(() => {
369
+ drawing = false;
370
+ });
234
371
  };
235
- input.on("line", (line) => {
372
+ const runCommand = async (line: string): Promise<void> => {
236
373
  const trimmed = line.trim();
237
- if (trimmed.startsWith("m ") && sessionSocket && sessionToken) {
238
- void dashboardSessionRequest(sessionSocket, sessionToken, { action: "set", edits: trimmed.slice(2).trim() })
239
- .then(() => {
240
- notice = "session model defaults updated";
241
- })
242
- .catch((error) => {
243
- notice = `model edit refused: ${error instanceof Error ? error.message : String(error)}`;
244
- })
245
- .finally(redraw);
374
+ if (trimmed.startsWith("m ")) {
375
+ notice = (await connection.change({ action: "set", edits: trimmed.slice(2).trim() }))
376
+ ? "Session models updated"
377
+ : connection.state.error || "Session controls unavailable";
378
+ } else notice = display.input(line) ? "" : `Unknown command: ${trimmed}. Press ? for help.`;
379
+ redraw();
380
+ };
381
+ let stop: () => void = () => {};
382
+ const keypress = (text: string | undefined, key: Key): void => {
383
+ if (key.ctrl && key.name === "c") {
384
+ stop();
385
+ return;
386
+ }
387
+ if (command !== undefined) {
388
+ if (key.name === "escape") command = undefined;
389
+ else if (key.name === "return") {
390
+ const line = command;
391
+ command = undefined;
392
+ void runCommand(line);
393
+ } else if (key.name === "backspace") command = [...command].slice(0, -1).join("");
394
+ else if (text && !key.ctrl && !key.meta && !/[\p{Cc}\p{Cf}]/u.test(text) && command.length < 2048)
395
+ command += text;
396
+ redraw();
397
+ return;
398
+ }
399
+ const action = display.key(
400
+ key.name === "return"
401
+ ? "return"
402
+ : ["up", "down", "escape", "tab"].includes(key.name || "")
403
+ ? key.name!
404
+ : text || "",
405
+ );
406
+ if (action === "quit") {
407
+ stop();
246
408
  return;
247
409
  }
248
- // Rule 8: a line the display controls do not understand is said so, never swallowed.
249
- notice = display.input(line) ? "" : `not a display command: ${JSON.stringify(trimmed)}. ${display.prompt()}`;
410
+ if (action === "command") command = "";
411
+ if (action === "auto") {
412
+ const snapshot = connection.state.snapshot;
413
+ if (!snapshot) notice = "Daddy permission controls unavailable";
414
+ else {
415
+ const enabled = !snapshot.auto.enabled;
416
+ void connection.change({ action: "set-auto", enabled }).then((changed) => {
417
+ notice = changed
418
+ ? `Daddy permission Auto ${enabled ? "ON" : "OFF"}; ${enabled ? "future prompts auto-approved" : "admitted work continues"}`
419
+ : connection.state.error || "Permission change already pending";
420
+ redraw();
421
+ });
422
+ }
423
+ }
250
424
  redraw();
251
- });
252
- const timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
253
- process.on("SIGWINCH", redraw);
254
- await new Promise<void>((settle) => {
255
- const stop = () => {
256
- clearInterval(timer);
257
- input?.close();
258
- process.off("SIGWINCH", redraw);
259
- settle();
260
- };
261
- process.once("SIGINT", stop);
262
- process.once("SIGTERM", stop);
263
- });
425
+ };
426
+ let timer: ReturnType<typeof setInterval> | undefined;
427
+ const oldRaw = process.stdin.isRaw;
428
+ try {
429
+ if (interactive) {
430
+ emitKeypressEvents(process.stdin);
431
+ process.stdin.setRawMode(true);
432
+ process.stdin.resume();
433
+ process.stdout.write("\u001b[?1049h\u001b[?25l");
434
+ } else {
435
+ input = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
436
+ input.on("line", (line) => {
437
+ void runCommand(line);
438
+ });
439
+ }
440
+ await new Promise<void>((settle) => {
441
+ stop = () => {
442
+ stopped = true;
443
+ settle();
444
+ };
445
+ process.once("SIGINT", stop);
446
+ process.once("SIGTERM", stop);
447
+ if (interactive) process.stdin.on("keypress", keypress);
448
+ process.on("SIGWINCH", redraw);
449
+ timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
450
+ redraw();
451
+ });
452
+ } finally {
453
+ stopped = true;
454
+ if (timer) clearInterval(timer);
455
+ process.off("SIGINT", stop);
456
+ process.off("SIGTERM", stop);
457
+ process.off("SIGWINCH", redraw);
458
+ input?.close();
459
+ if (interactive) {
460
+ process.stdin.off("keypress", keypress);
461
+ process.stdin.setRawMode(oldRaw || false);
462
+ process.stdin.pause();
463
+ process.stdout.write("\u001b[?25h\u001b[?1049l");
464
+ }
465
+ }
264
466
  }
265
467
 
266
468
  const invoked = (() => {
@@ -1,4 +1,5 @@
1
1
  /** Local display controls; these never dispatch actions to an owner or change ledger state. */
2
+ import { dashboardScreenState } from "./dashboard-screen.ts";
2
3
  import { ActivityTimelineAliases } from "./activity-timeline.ts";
3
4
 
4
5
  export function createDashboardDisplayControls(details: boolean, ledger: boolean) {
@@ -10,14 +11,63 @@ export function createDashboardDisplayControls(details: boolean, ledger: boolean
10
11
  } = { details, history: false, ...(ledger ? { filter: "everything" as const } : {}) };
11
12
  // Held by the panel process, so a refresh or a newly observed root cannot renumber an already displayed alias.
12
13
  const aliases = new ActivityTimelineAliases();
14
+ const screen = dashboardScreenState();
15
+ if (details) screen.view = "details";
13
16
  return {
14
17
  state,
18
+ screen,
19
+ key(key: string): "auto" | "quit" | "command" | undefined {
20
+ if (key === "a") return "auto";
21
+ if (key === "q" || key === "ctrl-c") return "quit";
22
+ if (key === ":") return "command";
23
+ if (key === "escape") {
24
+ screen.view = screen.view === "version-details" ? "versions" : "main";
25
+ state.activityDetail = undefined;
26
+ screen.offset = 0;
27
+ } else if (key === "?" || key === "m" || key === "v") {
28
+ screen.view = key === "?" ? "help" : key === "v" ? "versions" : "models";
29
+ state.activityDetail = undefined;
30
+ screen.offset = 0;
31
+ } else if (key === "return" || key === "d") {
32
+ screen.view = screen.view === "versions" ? "version-details" : "details";
33
+ screen.offset = 0;
34
+ } else if (["up", "down", "j", "k"].includes(key)) {
35
+ const delta = key === "up" || key === "k" ? -1 : 1;
36
+ if (screen.view === "versions") {
37
+ const rows = screen.versionRows ?? [],
38
+ index = rows.findIndex((row) => row.id === screen.versionId);
39
+ screen.versionId = rows[Math.max(0, Math.min(rows.length - 1, index + delta))]?.id;
40
+ } else if (screen.view !== "main") screen.offset = Math.max(0, screen.offset + delta);
41
+ else {
42
+ const index = Math.max(
43
+ 0,
44
+ screen.items.findIndex((item) => item.key === screen.selectedKey),
45
+ );
46
+ screen.selectedKey = screen.items[Math.max(0, Math.min(screen.items.length - 1, index + delta))]?.key;
47
+ state.activityDetail = undefined;
48
+ }
49
+ } else if (key === "tab") {
50
+ const filters = ["everything", "agents", "skills", "needs-you"] as const;
51
+ state.filter = filters[(filters.indexOf(state.filter ?? "everything") + 1) % filters.length];
52
+ screen.view = "main";
53
+ state.activityDetail = undefined;
54
+ } else if (key === "p" || key === "f") {
55
+ const selected = screen.items.find((item) => item.key === screen.selectedKey);
56
+ if (selected?.taskKey) {
57
+ state.activityDetail = { taskKey: selected.taskKey, field: key === "p" ? "prompt" : "final" };
58
+ screen.view = "details";
59
+ screen.offset = 0;
60
+ }
61
+ } else if (key === "h") state.history = !state.history;
62
+ return undefined;
63
+ },
15
64
  aliases,
16
65
  input(line: string): boolean {
17
66
  const input = line.trim(),
18
67
  key = input.toLowerCase();
19
68
  if (key === "d") {
20
69
  state.details = !state.details;
70
+ screen.view = state.details ? "details" : "main";
21
71
  return true;
22
72
  }
23
73
  if (ledger && key === "h") {
@@ -30,6 +80,8 @@ export function createDashboardDisplayControls(details: boolean, ledger: boolean
30
80
  }
31
81
  const detail = /^(p|f)\s+([^\s]{1,1400})$/i.exec(input);
32
82
  if (ledger && detail) {
83
+ screen.view = "details";
84
+ screen.offset = 0;
33
85
  state.activityDetail = { field: detail[1]!.toLowerCase() === "p" ? "prompt" : "final", taskKey: detail[2]! };
34
86
  return true;
35
87
  }
@@ -38,11 +38,11 @@ const displayIdentifier = (value: string | undefined): string => (isLedgerDispla
38
38
  const ANSI_SGR = /\u001b\[[0-9;]*m/g;
39
39
  const WIDE_CELL = /\p{Extended_Pictographic}|[\u2E80-\u9FFF\uF900-\uFAFF]/u;
40
40
 
41
- function cellWidth(value: string): number {
41
+ export function cellWidth(value: string): number {
42
42
  return [...value.replace(ANSI_SGR, "")].reduce((sum, char) => sum + (WIDE_CELL.test(char) ? 2 : 1), 0);
43
43
  }
44
44
 
45
- function truncate(value: string, width: number): string {
45
+ export function truncate(value: string, width: number): string {
46
46
  if (cellWidth(value) <= width) return value;
47
47
  const target = Math.max(0, width - 1);
48
48
  let cells = 0;