pi-daddy 0.46.1 → 0.47.1

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 +14 -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 +12 -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 +249 -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 +14 -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 +256 -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
@@ -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;
@@ -0,0 +1,339 @@
1
+ /** Height-bounded, task-first view. Permission state is always supplied by the owner. */
2
+ import type { EcosystemVersionRow } from "./ecosystem-versions.ts";
3
+ import type { DashboardProjection } from "./dashboard-projection.ts";
4
+ import { ActivityTimelineAliases, activityTaskKey, type ActivityTimeline } from "./activity-timeline.ts";
5
+ import { cellWidth, truncate } from "./dashboard-render.ts";
6
+ import type { DashboardSessionSnapshot } from "./dashboard-session-client.ts";
7
+
8
+ export interface DashboardItem {
9
+ key: string;
10
+ label: string;
11
+ status: string;
12
+ elapsed: string;
13
+ details: string[];
14
+ taskKey?: string;
15
+ timelinePath?: string;
16
+ approval?: boolean;
17
+ skills?: boolean;
18
+ agent?: boolean;
19
+ }
20
+ export interface DashboardScreenState {
21
+ view: "main" | "details" | "models" | "help" | "versions" | "version-details";
22
+ versionId?: EcosystemVersionRow["id"];
23
+ versionRows?: EcosystemVersionRow[];
24
+ selectedKey?: string;
25
+ offset: number;
26
+ items: DashboardItem[];
27
+ }
28
+ export const dashboardScreenState = (): DashboardScreenState => ({ view: "main", offset: 0, items: [] });
29
+ export const dashboardText = (value: string) =>
30
+ value
31
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
32
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
33
+ .replace(/[\p{Cc}\p{Cf}]/gu, " ");
34
+ const clean = (value: string) => dashboardText(value).replace(/\s+/g, " ").trim();
35
+ function wrapped(lines: string[], width: number): string[] {
36
+ return lines.flatMap((line) => {
37
+ const result: string[] = [];
38
+ let row = "";
39
+ for (const character of dashboardText(line)) {
40
+ if (row && cellWidth(row + character) > width) {
41
+ result.push(row);
42
+ row = "";
43
+ }
44
+ row += character;
45
+ }
46
+ result.push(row);
47
+ return result;
48
+ });
49
+ }
50
+ function elapsed(ms: number): string {
51
+ const seconds = Math.max(0, Math.floor(ms / 1000));
52
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
53
+ }
54
+ const friendlyStatus = (status: string) =>
55
+ ({
56
+ active: "Running",
57
+ running: "Running",
58
+ starting: "Starting",
59
+ authorised: "Approved",
60
+ "needs-you": "Needs you",
61
+ completed: "Finished",
62
+ finished: "Finished",
63
+ failed: "Failed",
64
+ refused: "Blocked",
65
+ incomplete: "Incomplete",
66
+ cancelled: "Canceled",
67
+ historical: "Recorded",
68
+ })[status] ?? status;
69
+ const versionStatus = (row: EcosystemVersionRow): string => {
70
+ if (row.state === "reload-required") return row.id === "pi" ? "Restart needed" : "Reload needed";
71
+ if (row.state === "not-reported") return "Loaded version unknown";
72
+ if (row.state === "multiple") return "Multiple sources";
73
+ if (row.state === "current" && row.loadedVersion && row.loadedVersion === row.installedVersion)
74
+ return "Up to date on disk";
75
+ return "Unavailable";
76
+ };
77
+ const attention = (status: string) => ["failed", "refused", "incomplete"].includes(status);
78
+ const active = (status: string) => ["active", "running", "starting", "authorised", "needs-you"].includes(status);
79
+ export function ledgerDashboardItems(projection: DashboardProjection): DashboardItem[] {
80
+ return projection.nodes.map((node) => ({
81
+ key: node.executionId,
82
+ label: node.agentName || "Governed execution",
83
+ status: node.state,
84
+ elapsed: elapsed(node.durationMs),
85
+ agent: true,
86
+ details: [
87
+ `Execution: ${node.executionId}`,
88
+ `State: ${friendlyStatus(node.state)} (observation, not acceptance)`,
89
+ `Grant: ${node.effectiveGrant.join(", ") || "none"}`,
90
+ ...(node.workspace ? [`Workspace: ${node.workspace.id} (${node.workspace.access})`] : []),
91
+ ...(node.runtime?.herdrPaneId ? [`Pane: ${node.runtime.herdrPaneId}`] : []),
92
+ ...(node.correlation?.phase ? [`Phase: ${node.correlation.phase}`] : []),
93
+ ...(node.executor ? [`Executor: ${node.executor}`] : []),
94
+ ],
95
+ }));
96
+ }
97
+ export function activityDashboardItems(
98
+ timeline: ActivityTimeline,
99
+ aliases: ActivityTimelineAliases,
100
+ now: Date,
101
+ ): DashboardItem[] {
102
+ return timeline.tasks.map((task) => ({
103
+ key: activityTaskKey(task.rootId, task.id),
104
+ taskKey: activityTaskKey(task.rootId, task.id),
105
+ label: `${task.agent || "User turn"} [${aliases.selector(task)}]`,
106
+ status: task.status,
107
+ elapsed: elapsed(Date.parse(task.endedAt ?? now.toISOString()) - Date.parse(task.startedAt)),
108
+ agent: Boolean(task.agent || task.agents.length),
109
+ skills: task.skills.length > 0,
110
+ details: [
111
+ `Task: ${aliases.selector(task)}`,
112
+ `State: ${friendlyStatus(task.status)} (observation, not acceptance)`,
113
+ ...(task.parentTaskId ? [`Parent: ${task.parentTaskId}`] : []),
114
+ ...(task.model ? [`Model: ${task.model}`] : []),
115
+ ...(task.thinking ? [`Thinking: ${task.thinking}`] : []),
116
+ ...task.skills.map(
117
+ (skill) =>
118
+ `Skill ${skill.name}: ${skill.active ? "declared active" : skill.read ? "read" : "available"}${skill.superseded ? ", superseded" : ""}`,
119
+ ),
120
+ ...(task.prompt || task.final ? ["p prompt / f final (retained content only)"] : []),
121
+ ],
122
+ }));
123
+ }
124
+ export interface DashboardScreenOptions {
125
+ width: number;
126
+ height: number;
127
+ screen: DashboardScreenState;
128
+ items: DashboardItem[];
129
+ session?: DashboardSessionSnapshot;
130
+ history?: boolean;
131
+ filter?: string;
132
+ notice?: string;
133
+ pending?: boolean;
134
+ problem?: string;
135
+ current?: string;
136
+ currentStatus?: string;
137
+ color?: boolean;
138
+ content?: { title: string; text: string };
139
+ command?: string;
140
+ }
141
+ /** Selection follows stable identity through refresh, never an unrelated task at the old row index. */
142
+ export function renderDashboardScreen(options: DashboardScreenOptions): string {
143
+ const width = Math.max(1, Math.floor(options.width)),
144
+ height = Math.max(1, Math.floor(options.height));
145
+ const { screen, session } = options,
146
+ pending = session?.pendingApprovals ?? [];
147
+ const approvals: DashboardItem[] = pending.map((approval) => ({
148
+ key: `approval:${approval.id}`,
149
+ label: `${approval.subject} · ${approval.capability.replace(/^tool:/, "")}`,
150
+ status: "needs-you",
151
+ elapsed: "",
152
+ approval: true,
153
+ details: [
154
+ `Approval: ${approval.id}`,
155
+ `Subject: ${approval.subject}`,
156
+ `Capability: ${approval.capability}`,
157
+ "Waiting for Daddy permission in the owner session.",
158
+ "Use the owner's prompt, or a to enable Auto.",
159
+ ],
160
+ }));
161
+ const allItems = [...approvals, ...options.items];
162
+ const items = allItems.filter((item) =>
163
+ options.filter === "agents"
164
+ ? item.agent
165
+ : options.filter === "skills"
166
+ ? item.skills
167
+ : options.filter === "needs-you"
168
+ ? item.status === "needs-you"
169
+ : true,
170
+ );
171
+ const live = items.filter((item) => active(item.status)),
172
+ needsAttention = items.filter((item) => attention(item.status)),
173
+ past = items.filter((item) => !active(item.status) && !attention(item.status)).reverse();
174
+ screen.items = [...live, ...needsAttention, ...(options.history ? past : [])];
175
+ if (screen.view === "details" && !screen.items.some((item) => item.key === screen.selectedKey)) {
176
+ const detail = items.find((item) => item.key === screen.selectedKey);
177
+ if (detail) screen.items.push(detail);
178
+ }
179
+ if (!screen.items.some((item) => item.key === screen.selectedKey)) screen.selectedKey = screen.items[0]?.key;
180
+ const selectedIndex = screen.items.findIndex((item) => item.key === screen.selectedKey),
181
+ selected = screen.items[selectedIndex];
182
+ screen.versionRows = session?.versions?.rows ?? [];
183
+ if (!screen.versionRows.some((row) => row.id === screen.versionId)) screen.versionId = screen.versionRows[0]?.id;
184
+ const selectedVersion = screen.versionRows.find((row) => row.id === screen.versionId);
185
+ const mode = session ? `Auto ${session.auto.enabled ? "ON" : "OFF"}` : "Auto unavailable";
186
+ const header = `PI DADDY | ${mode}${options.pending ? " …" : session ? " [a]" : ""}`;
187
+ const footer =
188
+ options.command !== undefined
189
+ ? [`: ${clean(options.command)}`]
190
+ : screen.view === "main" || screen.view === "versions"
191
+ ? ["↑↓ select · Enter details · a Auto", "m models · v versions · Tab filter · ?"]
192
+ : [
193
+ "↑↓ scroll · Esc back · a Auto",
194
+ screen.view === "details" ? "p prompt · f final · : command · ? help" : ": command · ? help",
195
+ ];
196
+ const fixed = [
197
+ header,
198
+ session
199
+ ? `Permissions connected${pending.length ? ` · ${pending.length} needs you` : ""}`
200
+ : "Permission controls disconnected",
201
+ ];
202
+ if (options.notice || options.problem) fixed.push(`! ${clean(options.notice || options.problem || "")}`);
203
+ const body: string[] = [];
204
+ if (screen.view === "help")
205
+ body.push(
206
+ "DASHBOARD KEYS",
207
+ "↑/↓ or j/k: select; Enter/d: details",
208
+ "a: toggle Daddy permission Auto",
209
+ "m: session models; v: ecosystem versions",
210
+ "h: show/hide finished history",
211
+ "Tab: Everything / Agents / Skills / Needs-you",
212
+ "p/f: selected retained prompt/final",
213
+ ": opens commands (legacy selectors and m edits)",
214
+ "Esc: back; q/Ctrl-C: close dashboard",
215
+ "Auto changes future Daddy permission handling.",
216
+ "OFF does not cancel already admitted work.",
217
+ "Auto does not enable JEV or change LoRA consent.",
218
+ "Finished means observed completion, not acceptance.",
219
+ );
220
+ else if (screen.view === "versions" || screen.view === "version-details") {
221
+ body.push("VERSIONS");
222
+ if (!session?.versions) body.push("Owner has not reported versions.");
223
+ else if (screen.view === "versions") {
224
+ body.push("Loaded -> on disk");
225
+ for (const row of screen.versionRows)
226
+ body.push(
227
+ `${row.id === screen.versionId ? ">" : " "} ${row.label}: ${row.loadedVersion ?? "?"} -> ${row.installedVersion ?? "?"}`,
228
+ ` ${versionStatus(row)}`,
229
+ );
230
+ if (session.versions.note) body.push(session.versions.note);
231
+ } else if (selectedVersion) {
232
+ body.push(
233
+ `${selectedVersion.label}: ${versionStatus(selectedVersion)}`,
234
+ `Loaded: ${selectedVersion.loadedVersion ?? "not reported"}`,
235
+ `Installed: ${selectedVersion.installedVersion ?? "unavailable"}`,
236
+ `Source: ${selectedVersion.source ?? "unavailable"}`,
237
+ `Path: ${selectedVersion.path ?? "unavailable"}`,
238
+ "",
239
+ "MANAGE · run in a terminal",
240
+ ...selectedVersion.commands,
241
+ );
242
+ if (selectedVersion.note) body.push(selectedVersion.note);
243
+ if (session.versions.note) body.push(session.versions.note);
244
+ }
245
+ } else if (screen.view === "models") {
246
+ body.push("SESSION MODELS");
247
+ if (!session) body.push("Connect from /grants dashboard to edit models.");
248
+ for (const row of session?.rows ?? [])
249
+ body.push(`${row.definition}: ${row.model}`, ` ${row.thinking} · ${row.source}`);
250
+ body.push(": m <definition|all> <provider:model> <thinking>");
251
+ } else if (screen.view === "details") {
252
+ body.push(
253
+ selected ? `${selected.label} · ${friendlyStatus(selected.status)}` : "No selected task",
254
+ ...(selected?.details ?? []),
255
+ );
256
+ if (options.content) body.push("", options.content.title, ...options.content.text.split(/\r?\n/));
257
+ } else {
258
+ const requestFinished = options.currentStatus && !active(options.currentStatus);
259
+ body.push(
260
+ options.current
261
+ ? requestFinished
262
+ ? `LATEST REQUEST · ${friendlyStatus(options.currentStatus!)}`
263
+ : "CURRENT REQUEST"
264
+ : "CURRENT WORK",
265
+ clean(options.current ?? live.find((item) => !item.approval)?.label ?? "No active work"),
266
+ "",
267
+ );
268
+ const rows: { text: string; key?: string }[] = [];
269
+ let group: string | undefined;
270
+ for (const item of screen.items) {
271
+ const next = item.approval
272
+ ? "NEEDS YOU"
273
+ : attention(item.status)
274
+ ? "NEEDS ATTENTION"
275
+ : active(item.status)
276
+ ? `WORK IN PROGRESS · ${live.filter((value) => !value.approval).length} active`
277
+ : "HISTORY";
278
+ if (next !== group) {
279
+ rows.push({ text: next });
280
+ group = next;
281
+ }
282
+ const tail = `${friendlyStatus(item.status)} ${item.elapsed}`.trim(),
283
+ labelWidth = Math.max(1, width - tail.length - 4);
284
+ rows.push({
285
+ key: item.key,
286
+ text: `${item.key === screen.selectedKey ? ">" : " "} ${truncate(clean(item.label), labelWidth)} ${tail}`,
287
+ });
288
+ }
289
+ const hidden = !options.history ? past.length : 0;
290
+ const history = hidden ? `${hidden} finished · h shows history` : options.history ? "h hides history" : "";
291
+ const available = Math.max(0, height - fixed.length - footer.length - body.length - (history ? 1 : 0));
292
+ const rowIndex = rows.findIndex((row) => row.key === selected?.key);
293
+ const start = Math.max(0, rowIndex - available + 1);
294
+ body.push(...rows.slice(start, start + available).map((row) => row.text));
295
+ if (!rows.length)
296
+ body.push(options.filter && options.filter !== "everything" ? "No matching active work." : "No work is running.");
297
+ if (history) body.push(history);
298
+ }
299
+
300
+ const detailBody = screen.view === "main" || screen.view === "versions" ? body : wrapped(body, width);
301
+ const room = Math.max(0, height - fixed.length - footer.length);
302
+ if (screen.view === "versions" && selectedVersion && room > 0) {
303
+ const row = 2 + screen.versionRows.findIndex((value) => value.id === screen.versionId) * 2;
304
+ if (row < screen.offset) screen.offset = row;
305
+ else if (row + 1 >= screen.offset + room) screen.offset = Math.max(0, row + 2 - room);
306
+ }
307
+ const offset =
308
+ screen.view === "main" ? 0 : Math.max(0, Math.min(screen.offset, Math.max(0, detailBody.length - room)));
309
+ screen.offset = offset;
310
+ const lines = [...fixed, ...detailBody.slice(offset, offset + room)];
311
+ while (lines.length < height - footer.length) lines.push("");
312
+ const output =
313
+ height < fixed.length + footer.length
314
+ ? fixed.slice(0, height)
315
+ : [...lines.slice(0, Math.max(0, height - footer.length)), ...footer].slice(0, height);
316
+ return output
317
+ .map((line, index) => {
318
+ const safe = truncate(dashboardText(line), width);
319
+ if (!options.color) return safe;
320
+ const section =
321
+ /^(CURRENT REQUEST|LATEST REQUEST|CURRENT WORK|NEEDS YOU|NEEDS ATTENTION|WORK IN PROGRESS|HISTORY|VERSIONS|SESSION MODELS|DASHBOARD KEYS|MANAGE)/.test(
322
+ safe,
323
+ );
324
+ const code =
325
+ index === 0
326
+ ? "1;36"
327
+ : safe.startsWith("!")
328
+ ? "33"
329
+ : safe.startsWith(">")
330
+ ? "36"
331
+ : section
332
+ ? "1"
333
+ : index === 1 || index >= height - footer.length
334
+ ? "90"
335
+ : undefined;
336
+ return code ? `\u001b[${code}m${safe}\u001b[0m` : safe;
337
+ })
338
+ .join("\n");
339
+ }
@@ -1,14 +1,22 @@
1
+ import type { EcosystemVersions } from "./ecosystem-versions.ts";
1
2
  import { createConnection } from "node:net";
2
3
 
3
4
  export interface DashboardSessionSnapshot {
4
5
  rows: Array<{ definition: string; model: string; thinking: string; source: string }>;
5
6
  cost: number | null;
7
+ activity?: { rootId: string; path: string; taskId: string };
8
+ versions?: EcosystemVersions;
9
+ auto: { enabled: boolean; source: "default" | "environment" | "session" };
10
+ pendingApprovals: Array<{ id: string; subject: string; capability: string }>;
6
11
  }
7
12
 
13
+ export type DashboardSessionAction =
14
+ { action: "get" } | { action: "set"; edits: string } | { action: "set-auto"; enabled: boolean };
15
+
8
16
  export async function dashboardSessionRequest(
9
17
  socketPath: string,
10
18
  token: string,
11
- request: { action: "get" } | { action: "set"; edits: string },
19
+ request: DashboardSessionAction,
12
20
  ): Promise<DashboardSessionSnapshot> {
13
21
  return new Promise((resolve, reject) => {
14
22
  const socket = createConnection(socketPath);
@@ -24,6 +32,14 @@ export async function dashboardSessionRequest(
24
32
  try {
25
33
  const parsed = JSON.parse(output) as DashboardSessionSnapshot & { ok?: boolean; error?: string };
26
34
  if (!parsed.ok) throw new Error(parsed.error ?? "dashboard session request failed");
35
+ if (
36
+ !parsed.auto ||
37
+ typeof parsed.auto.enabled !== "boolean" ||
38
+ !["default", "environment", "session"].includes(parsed.auto.source) ||
39
+ !Array.isArray(parsed.pendingApprovals) ||
40
+ !Array.isArray(parsed.rows)
41
+ )
42
+ throw new Error("dashboard owner does not provide compatible permission controls");
27
43
  resolve(parsed);
28
44
  } catch (error) {
29
45
  reject(error);
@@ -32,3 +48,46 @@ export async function dashboardSessionRequest(
32
48
  socket.once("connect", () => socket.write(`${JSON.stringify({ ...request, token })}\n`));
33
49
  });
34
50
  }
51
+
52
+ /** A slow status poll cannot overwrite a newer acknowledged mutation. Failed requests expose no guessed mode. */
53
+ export function createDashboardConnection(
54
+ request?: (action: DashboardSessionAction) => Promise<DashboardSessionSnapshot>,
55
+ ) {
56
+ const state: { snapshot?: DashboardSessionSnapshot; pending: boolean; error?: string } = { pending: false };
57
+ let generation = 0;
58
+ return {
59
+ state,
60
+ async refresh() {
61
+ if (!request || state.pending) return;
62
+ const observed = generation;
63
+ try {
64
+ const snapshot = await request({ action: "get" });
65
+ if (generation === observed) {
66
+ state.snapshot = snapshot;
67
+ state.error = undefined;
68
+ }
69
+ } catch (error) {
70
+ if (generation === observed) {
71
+ state.snapshot = undefined;
72
+ state.error = `Controls unavailable: ${error instanceof Error ? error.message : String(error)}`;
73
+ }
74
+ }
75
+ },
76
+ async change(action: Exclude<DashboardSessionAction, { action: "get" }>) {
77
+ if (!request || !state.snapshot || state.pending) return false;
78
+ generation++;
79
+ state.pending = true;
80
+ try {
81
+ state.snapshot = await request(action);
82
+ state.error = undefined;
83
+ return true;
84
+ } catch (error) {
85
+ state.snapshot = undefined;
86
+ state.error = `Control change not confirmed: ${error instanceof Error ? error.message : String(error)}`;
87
+ return false;
88
+ } finally {
89
+ state.pending = false;
90
+ }
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,18 @@
1
+ /** Operator version inventory. These observations do not qualify runtime compatibility. */
2
+ export type EcosystemPackageId = "pi" | "pi-daddy" | "principal-pi-skills" | "skill-harness";
3
+ export interface EcosystemVersionRow {
4
+ id: EcosystemPackageId;
5
+ label: string;
6
+ loadedVersion: string | null;
7
+ installedVersion: string | null;
8
+ source: string | null;
9
+ path: string | null;
10
+ state: "current" | "reload-required" | "not-reported" | "unavailable" | "multiple";
11
+ commands: string[];
12
+ note?: string;
13
+ }
14
+ export interface EcosystemVersions {
15
+ rows: EcosystemVersionRow[];
16
+ checkedAt: string;
17
+ note?: string;
18
+ }