@agimon-ai/po-plan 0.0.0-alpha.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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/apiContracts.cjs +57 -0
  4. package/dist/apiContracts.cjs.map +1 -0
  5. package/dist/apiContracts.d.mts +53 -0
  6. package/dist/apiContracts.d.mts.map +1 -0
  7. package/dist/apiContracts.mjs +52 -0
  8. package/dist/apiContracts.mjs.map +1 -0
  9. package/dist/config-BC-Tou05.mjs +25 -0
  10. package/dist/config-BC-Tou05.mjs.map +1 -0
  11. package/dist/config-D0iTexGm.cjs +42 -0
  12. package/dist/config-D0iTexGm.cjs.map +1 -0
  13. package/dist/config-DWo57HKB.d.mts +13 -0
  14. package/dist/config-DWo57HKB.d.mts.map +1 -0
  15. package/dist/config.cjs +12 -0
  16. package/dist/config.d.mts +2 -0
  17. package/dist/config.mjs +2 -0
  18. package/dist/extensions/mcp-ui.mjs +46 -0
  19. package/dist/extensions/mcp-ui.mjs.map +1 -0
  20. package/dist/extensions/mcp.cjs +93 -0
  21. package/dist/extensions/mcp.cjs.map +1 -0
  22. package/dist/extensions/mcp.d.mts +5 -0
  23. package/dist/extensions/mcp.d.mts.map +1 -0
  24. package/dist/extensions/mcp.mjs +88 -0
  25. package/dist/extensions/mcp.mjs.map +1 -0
  26. package/dist/extensions/server.cjs +1025 -0
  27. package/dist/extensions/server.cjs.map +1 -0
  28. package/dist/extensions/server.d.mts +5 -0
  29. package/dist/extensions/server.d.mts.map +1 -0
  30. package/dist/extensions/server.mjs +993 -0
  31. package/dist/extensions/server.mjs.map +1 -0
  32. package/dist/extensions/web.mjs +861 -0
  33. package/dist/extensions/web.mjs.map +1 -0
  34. package/dist/fableFlow-BDc8a__Y.mjs +214 -0
  35. package/dist/fableFlow-BDc8a__Y.mjs.map +1 -0
  36. package/dist/fableFlow-CzfrLPzJ.cjs +279 -0
  37. package/dist/fableFlow-CzfrLPzJ.cjs.map +1 -0
  38. package/dist/fableFlow.cjs +13 -0
  39. package/dist/fableFlow.d.mts +2 -0
  40. package/dist/fableFlow.mjs +2 -0
  41. package/dist/index-C8e7IK60.d.mts +68 -0
  42. package/dist/index-C8e7IK60.d.mts.map +1 -0
  43. package/dist/index-C9QBG9cw.d.mts +51 -0
  44. package/dist/index-C9QBG9cw.d.mts.map +1 -0
  45. package/dist/index.cjs +31 -0
  46. package/dist/index.d.mts +4 -0
  47. package/dist/index.mjs +4 -0
  48. package/dist/logSinkTelemetry.cjs +42 -0
  49. package/dist/logSinkTelemetry.cjs.map +1 -0
  50. package/dist/logSinkTelemetry.d.mts +33 -0
  51. package/dist/logSinkTelemetry.d.mts.map +1 -0
  52. package/dist/logSinkTelemetry.mjs +40 -0
  53. package/dist/logSinkTelemetry.mjs.map +1 -0
  54. package/dist/planConfig.cjs +190 -0
  55. package/dist/planConfig.cjs.map +1 -0
  56. package/dist/planConfig.d.mts +47 -0
  57. package/dist/planConfig.d.mts.map +1 -0
  58. package/dist/planConfig.mjs +186 -0
  59. package/dist/planConfig.mjs.map +1 -0
  60. package/dist/prompts-DUVUPNZd.cjs +103 -0
  61. package/dist/prompts-DUVUPNZd.cjs.map +1 -0
  62. package/dist/prompts-DnVJGtEC.mjs +68 -0
  63. package/dist/prompts-DnVJGtEC.mjs.map +1 -0
  64. package/dist/prompts.cjs +8 -0
  65. package/dist/prompts.d.mts +2 -0
  66. package/dist/prompts.mjs +2 -0
  67. package/llms.txt +9 -0
  68. package/package.json +164 -0
  69. package/src/prompts/po-use-plan/SKILL.md +44 -0
  70. package/src/types/planApi.ts +125 -0
  71. package/src/types/planSettings.ts +78 -0
@@ -0,0 +1,861 @@
1
+ import { apiResponse, createApiClient, defineActivityGroup, defineApiRoutes, defineFill, defineLeaderBinding, defineMinorModeFile, defineSettingsSection, defineToolRenderer, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
2
+ import { Button, CodeEditor, Kbd, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageLines, OptionList, SectionLabel, handleOptionListKey, optionListHint, toolTone } from "@agimon-ai/po-web-components";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { sealedTransport } from "@agimon-ai/po-web-security/browser";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/types/planSettings.ts
7
+ /**
8
+ * The planning settings, described once for both surfaces.
9
+ *
10
+ * The terminal panel and the cockpit settings page render the same five fields,
11
+ * and the one thing they must agree on is which config key each one writes. So
12
+ * the ids, labels and key paths live here rather than in either renderer.
13
+ *
14
+ * It lives under src/types because that is the one server root a browser bundle
15
+ * may read, which also means it may import nothing: pulling the config package
16
+ * in for the thinking levels would drag a node module into the page. They are
17
+ * spelled out instead, and a test asserts they still match the parser's.
18
+ */
19
+ const PLAN_CONFIG_SECTION_ID = "planning";
20
+ const PLANNING_PATH = ["modes", PLAN_CONFIG_SECTION_ID];
21
+ const MAIN_KEY = "main";
22
+ const SUBAGENTS_KEY = "subagents";
23
+ /** Both agents fall back to the session's own settings when left unset. */
24
+ const INHERIT_MODEL = "inherit the session model";
25
+ const INHERIT_THINKING = "inherit";
26
+ /** Mirrors DOOM_PLANNING_THINKING_LEVELS; pinned by planConfig's test. */
27
+ const PLAN_THINKING_LEVELS = [
28
+ "off",
29
+ "minimal",
30
+ "low",
31
+ "medium",
32
+ "high",
33
+ "xhigh",
34
+ "max"
35
+ ];
36
+ //#endregion
37
+ //#region src/extensions/(frontend)/_lib/planSettings.ts
38
+ /**
39
+ * The planning settings, as the cockpit's settings page renders them.
40
+ *
41
+ * Built from the same descriptor table the terminal panel uses, so the two
42
+ * surfaces cannot disagree about which config key a field writes. The host owns
43
+ * the scope switch and the writes; this only says what the fields are.
44
+ */
45
+ const planSettingsSection = {
46
+ id: PLAN_CONFIG_SECTION_ID,
47
+ label: "planning",
48
+ detail: "the models plan mode switches to, and where written plans land",
49
+ order: 30,
50
+ fields: [
51
+ {
52
+ id: "main.model",
53
+ label: "main model",
54
+ keyPath: [
55
+ ...PLANNING_PATH,
56
+ MAIN_KEY,
57
+ "model"
58
+ ],
59
+ placeholder: INHERIT_MODEL,
60
+ detail: "model the main agent switches to while plan mode is on.",
61
+ models: true
62
+ },
63
+ {
64
+ id: "main.thinking",
65
+ label: "main thinking",
66
+ keyPath: [
67
+ ...PLANNING_PATH,
68
+ MAIN_KEY,
69
+ "thinking"
70
+ ],
71
+ placeholder: INHERIT_THINKING,
72
+ detail: "thinking level appended to the main model while planning.",
73
+ options: PLAN_THINKING_LEVELS
74
+ },
75
+ {
76
+ id: "subagents.model",
77
+ label: "subagent model",
78
+ keyPath: [
79
+ ...PLANNING_PATH,
80
+ SUBAGENTS_KEY,
81
+ "model"
82
+ ],
83
+ placeholder: INHERIT_MODEL,
84
+ detail: "model forced onto delegated planning subagents.",
85
+ models: true
86
+ },
87
+ {
88
+ id: "subagents.thinking",
89
+ label: "subagent thinking",
90
+ keyPath: [
91
+ ...PLANNING_PATH,
92
+ SUBAGENTS_KEY,
93
+ "thinking"
94
+ ],
95
+ placeholder: INHERIT_THINKING,
96
+ detail: "thinking level appended to the subagent model.",
97
+ options: PLAN_THINKING_LEVELS
98
+ },
99
+ {
100
+ id: "plansdirectory",
101
+ label: "plans directory",
102
+ keyPath: [...PLANNING_PATH, "plansDirectory"],
103
+ placeholder: "~/.po/plans",
104
+ detail: "absolute, repo-relative, or under ~. written plans land here."
105
+ }
106
+ ].map((shape) => ({
107
+ id: shape.id,
108
+ label: shape.label,
109
+ kind: shape.models === true || shape.options !== void 0 ? "select" : "text",
110
+ keyPath: shape.keyPath,
111
+ detail: shape.detail,
112
+ placeholder: shape.placeholder,
113
+ ...shape.models === true ? { optionsFrom: "models" } : {},
114
+ ...shape.options === void 0 ? {} : { options: shape.options.map((option) => ({
115
+ value: option,
116
+ label: option
117
+ })) }
118
+ }))
119
+ };
120
+ //#endregion
121
+ //#region src/extensions/(frontend)/setting/plan.web.ts
122
+ var plan_web_default$3 = defineSettingsSection(planSettingsSection);
123
+ //#endregion
124
+ //#region src/extensions/workspaces/(frontend)/setting/plan.web.ts
125
+ var plan_web_default$2 = defineSettingsSection(planSettingsSection);
126
+ //#endregion
127
+ //#region src/types/planApi.ts
128
+ /**
129
+ * Footer status key the session publishes once a plan exists, which is what
130
+ * makes the cockpit's activity group appear at all. It outlives plan mode: the
131
+ * plan is most worth reading while the agent implements it.
132
+ */
133
+ const PLAN_STATUS_KEY = "doom-plan-document";
134
+ /** The same options as bare labels, which is all Pi's select and the composer prompt need. */
135
+ const PLAN_REVIEW_OPTIONS = [{
136
+ label: "Exit plan mode and start implementation",
137
+ value: "exit"
138
+ }, {
139
+ label: "Continue planning",
140
+ value: "continue"
141
+ }].map((choice) => choice.label);
142
+ /** Between the plan's title and the stamp that marks a rewrite. */
143
+ const STATUS_SEPARATOR = " · ";
144
+ /** Strips the colour a session may have themed a status with. */
145
+ const ANSI = /\[[0-9;]*m/gu;
146
+ /**
147
+ * Reading the status line back. Statuses reach a plugin raw, so the colour a
148
+ * session may have added comes off here. A title can itself contain the
149
+ * separator, so the stamp is taken from the last one rather than the first.
150
+ */
151
+ function parsePlanStatus(raw) {
152
+ if (raw === void 0) return void 0;
153
+ const text = raw.replace(ANSI, "").trim();
154
+ if (text === "") return void 0;
155
+ const cut = text.lastIndexOf(STATUS_SEPARATOR);
156
+ if (cut === -1) return {
157
+ title: text,
158
+ stamp: ""
159
+ };
160
+ return {
161
+ title: text.slice(0, cut).trim(),
162
+ stamp: text.slice(cut + 3).trim()
163
+ };
164
+ }
165
+ //#endregion
166
+ //#region src/extensions/workspaces/sessions/(frontend)/activity-group/plan.web.ts
167
+ var plan_web_default$1 = defineActivityGroup({
168
+ name: "plan",
169
+ keys: "p e",
170
+ statusKey: PLAN_STATUS_KEY,
171
+ marksBackgroundWork: false,
172
+ order: 25
173
+ });
174
+ //#endregion
175
+ //#region src/types/apiRoutes.ts
176
+ /**
177
+ * This package's routes, as data.
178
+ *
179
+ * No scope and no base path. The build reads both off
180
+ * `src/extensions/workspaces/sessions/(backend)/api/plans/`, so the mount is
181
+ * stated once, by the folder that creates it. The paths used to be written
182
+ * here as helpers, again in the contract, and again inside every URL the page
183
+ * built, and the copies were free to disagree.
184
+ *
185
+ * This is the one file the routes, the Hono app and the browser all read, so a
186
+ * path cannot move on one side alone.
187
+ *
188
+ * Neither route takes a query or a path parameter: a session has one current
189
+ * plan, so `current` answers it and `save` takes the manual write back.
190
+ */
191
+ var apiRoutes_default = defineApiRoutes({
192
+ current: {
193
+ method: "GET",
194
+ path: "/current",
195
+ response: apiResponse()
196
+ },
197
+ save: {
198
+ method: "PUT",
199
+ path: "/content",
200
+ response: apiResponse()
201
+ }
202
+ });
203
+ const api = createApiClient(apiRoutes_default, {
204
+ scopes: ["session"],
205
+ basePath: "plans",
206
+ transport: (input, init) => sealedTransport.fetch(input, init),
207
+ sessionAddress: sessionApiAddress
208
+ });
209
+ //#endregion
210
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_lib/planApi.ts
211
+ /**
212
+ * The page's half of this package's session API: the current plan, and the
213
+ * manual save.
214
+ *
215
+ * These are adapters now, not a transport. The generated client owns the URL
216
+ * and the sealed transport with it, so nothing here spells a route or reaches
217
+ * for `fetch`. What stays is this package's own vocabulary: which refusals the
218
+ * reader is shown, and the stale-save contract the editor depends on.
219
+ */
220
+ const UNREACHABLE = "The session is unreachable.";
221
+ const NO_PLAN = "This session has not written a plan yet.";
222
+ function isRecord(value) {
223
+ return typeof value === "object" && value !== null && !Array.isArray(value);
224
+ }
225
+ /** The message a reader is shown: the route's own words, or why there were none. */
226
+ function messageOf(result) {
227
+ if (result.status === 0) return UNREACHABLE;
228
+ return result.error === "" ? `The session answered ${String(result.status)}.` : result.error;
229
+ }
230
+ async function fetchPlan(sessionId) {
231
+ const result = await api.session(sessionId).current();
232
+ if (result.status === 404) return {
233
+ ok: false,
234
+ error: result.ok || result.error === "" ? NO_PLAN : result.error
235
+ };
236
+ if (!result.ok) return {
237
+ ok: false,
238
+ error: messageOf(result)
239
+ };
240
+ if (!isRecord(result.data)) return {
241
+ ok: false,
242
+ error: "The session answered with no plan."
243
+ };
244
+ return {
245
+ ok: true,
246
+ detail: result.data
247
+ };
248
+ }
249
+ async function savePlan(sessionId, expectedHash, content) {
250
+ const result = await api.session(sessionId).save({ body: {
251
+ expectedHash,
252
+ content
253
+ } });
254
+ if (result.status === 409) {
255
+ const data = result.ok ? void 0 : result.data;
256
+ const hash = isRecord(data) && typeof data.hash === "string" ? data.hash : void 0;
257
+ return {
258
+ ok: false,
259
+ stale: true,
260
+ error: result.ok ? "The plan changed since it was opened." : messageOf(result),
261
+ ...hash === void 0 ? {} : { hash }
262
+ };
263
+ }
264
+ if (!result.ok) return {
265
+ ok: false,
266
+ stale: false,
267
+ error: messageOf(result)
268
+ };
269
+ return {
270
+ ok: true,
271
+ hash: isRecord(result.data) && typeof result.data.hash === "string" ? result.data.hash : ""
272
+ };
273
+ }
274
+ //#endregion
275
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/PlanPanel.tsx
276
+ /** One tab per session; the plan is singular, so the id needs nothing else. */
277
+ const PLAN_TAB_ID = "plan-document";
278
+ /** The one note that is not an error: anything else in that slot failed. */
279
+ const SAVED_NOTE = "saved";
280
+ function PlanPanel({ sessionId, statuses, closeTransientTab }) {
281
+ const [detail, setDetail] = useState(void 0);
282
+ const [error, setError] = useState(void 0);
283
+ const [loading, setLoading] = useState(false);
284
+ const [view, setView] = useState("preview");
285
+ const [draft, setDraft] = useState(void 0);
286
+ const [saving, setSaving] = useState(false);
287
+ const [saveNote, setSaveNote] = useState(void 0);
288
+ const revision = statuses["doom-plan-document"] ?? "";
289
+ useEffect(() => {
290
+ if (sessionId === null) return;
291
+ let cancelled = false;
292
+ setLoading(true);
293
+ fetchPlan(sessionId).then((result) => {
294
+ if (cancelled) return;
295
+ setLoading(false);
296
+ if (result.ok) {
297
+ setDetail(result.detail);
298
+ setError(void 0);
299
+ setDraft(void 0);
300
+ setSaveNote(void 0);
301
+ return;
302
+ }
303
+ setDetail(void 0);
304
+ setError(result.error);
305
+ });
306
+ return () => {
307
+ cancelled = true;
308
+ };
309
+ }, [sessionId, revision]);
310
+ const content = draft ?? detail?.content ?? "";
311
+ const save = async () => {
312
+ if (sessionId === null || detail === void 0 || draft === void 0) return;
313
+ setSaving(true);
314
+ const result = await savePlan(sessionId, detail.hash, draft);
315
+ setSaving(false);
316
+ if (result.ok) {
317
+ setSaveNote(SAVED_NOTE);
318
+ setDraft(void 0);
319
+ const refreshed = await fetchPlan(sessionId);
320
+ if (refreshed.ok) setDetail(refreshed.detail);
321
+ else setError(refreshed.error);
322
+ return;
323
+ }
324
+ setSaveNote(result.error);
325
+ };
326
+ return /* @__PURE__ */ jsxs("div", {
327
+ "data-testid": "plan-panel",
328
+ className: "flex min-h-0 flex-1 flex-col",
329
+ children: [
330
+ /* @__PURE__ */ jsxs("header", {
331
+ className: "flex shrink-0 items-center gap-2 border-b border-doom-border px-4 py-2",
332
+ children: [
333
+ /* @__PURE__ */ jsx("span", {
334
+ "data-testid": "plan-title",
335
+ className: "min-w-0 flex-1 truncate text-lg font-semibold text-doom-hi",
336
+ children: detail?.title ?? "plan"
337
+ }),
338
+ /* @__PURE__ */ jsx("nav", {
339
+ className: "flex shrink-0 items-center gap-1",
340
+ children: ["preview", "source"].map((entry) => /* @__PURE__ */ jsx(Button, {
341
+ variant: view === entry ? "outline" : "ghost",
342
+ size: "xs",
343
+ "data-testid": `plan-view-${entry}`,
344
+ onClick: () => setView(entry),
345
+ children: entry
346
+ }, entry))
347
+ }),
348
+ /* @__PURE__ */ jsx(Button, {
349
+ variant: "ghost",
350
+ size: "xs",
351
+ "data-testid": "plan-close",
352
+ onClick: () => closeTransientTab(PLAN_TAB_ID),
353
+ className: "shrink-0",
354
+ children: "close"
355
+ })
356
+ ]
357
+ }),
358
+ loading && detail === void 0 ? /* @__PURE__ */ jsx("p", {
359
+ "data-testid": "plan-loading",
360
+ className: "px-4 py-3 text-sm text-doom-dim",
361
+ children: "reading this session's plan…"
362
+ }) : null,
363
+ error === void 0 ? null : /* @__PURE__ */ jsx("p", {
364
+ "data-testid": "plan-error",
365
+ className: "px-4 py-3 text-sm text-doom-red",
366
+ children: error
367
+ }),
368
+ /* @__PURE__ */ jsx("div", {
369
+ className: "flex min-h-0 flex-1 flex-col",
370
+ children: detail === void 0 ? null : detail.unavailable ? /* @__PURE__ */ jsx("p", {
371
+ "data-testid": "plan-unavailable",
372
+ className: "px-4 py-3 text-sm text-doom-dim",
373
+ children: detail.reason ?? "this plan cannot be shown"
374
+ }) : view === "preview" ? /* @__PURE__ */ jsx("div", {
375
+ "data-testid": "plan-preview",
376
+ className: "flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-4 text-base text-doom-text",
377
+ children: /* @__PURE__ */ jsx(Markdown, { text: content })
378
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(CodeEditor, {
379
+ "data-testid": "plan-source",
380
+ value: content,
381
+ path: detail.path,
382
+ onChange: setDraft,
383
+ className: "min-h-0 flex-1"
384
+ }), /* @__PURE__ */ jsxs("div", {
385
+ className: "flex shrink-0 items-center gap-2 border-t border-doom-border px-3 py-2",
386
+ children: [
387
+ /* @__PURE__ */ jsx(Button, {
388
+ variant: "outline",
389
+ size: "xs",
390
+ "data-testid": "plan-save",
391
+ loading: saving,
392
+ disabled: draft === void 0 || saving,
393
+ onClick: () => void save(),
394
+ children: "save to disk"
395
+ }),
396
+ draft === void 0 ? null : /* @__PURE__ */ jsx(Button, {
397
+ variant: "ghost",
398
+ size: "xs",
399
+ "data-testid": "plan-revert",
400
+ onClick: () => setDraft(void 0),
401
+ children: "discard edits"
402
+ }),
403
+ saveNote === void 0 ? null : /* @__PURE__ */ jsx("span", {
404
+ "data-testid": "plan-save-note",
405
+ className: saveNote === SAVED_NOTE ? "text-sm text-doom-green" : "text-sm text-doom-red",
406
+ children: saveNote
407
+ })
408
+ ]
409
+ })] })
410
+ }),
411
+ detail === void 0 ? null : /* @__PURE__ */ jsx("footer", {
412
+ "data-testid": "plan-source-path",
413
+ className: "shrink-0 truncate border-t border-doom-border px-4 py-1.5",
414
+ children: /* @__PURE__ */ jsx("span", {
415
+ className: "font-mono text-sm text-doom-dim",
416
+ children: detail.path
417
+ })
418
+ })
419
+ ]
420
+ });
421
+ }
422
+ /** The transient tab the plan opens in; opening it again only focuses it. */
423
+ function planTab() {
424
+ return {
425
+ id: PLAN_TAB_ID,
426
+ label: "plan",
427
+ panel: PlanPanel
428
+ };
429
+ }
430
+ //#endregion
431
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/PlanActivitySection.tsx
432
+ /**
433
+ * The plan group's body in the activity dock: the plan this session wrote, and
434
+ * the way in.
435
+ *
436
+ * A plan is one document, so the group is one row rather than a list. The row
437
+ * is the whole point of the group: a written plan the reader cannot open is a
438
+ * path in a tool card, which is what this replaces.
439
+ *
440
+ * The session's status line carries the title and the time it was last
441
+ * written, so the row says what the plan is and whether the agent has revised
442
+ * it since the reader last looked, without the dock holding any state.
443
+ */
444
+ function PlanActivitySection({ sessionId, statuses, openTransientTab }) {
445
+ const view = parsePlanStatus(statuses[PLAN_STATUS_KEY]);
446
+ if (view === void 0) return /* @__PURE__ */ jsx("p", {
447
+ "data-testid": "activity-summary-plan",
448
+ className: "px-1 text-sm text-doom-dim",
449
+ children: "no plan written yet"
450
+ });
451
+ return /* @__PURE__ */ jsxs("div", {
452
+ "data-testid": "activity-plan",
453
+ className: "flex flex-col gap-0.5",
454
+ children: [/* @__PURE__ */ jsx(Button, {
455
+ variant: "ghost",
456
+ size: "card",
457
+ "data-testid": "activity-plan-open",
458
+ title: `open ${view.title}`,
459
+ disabled: sessionId === null,
460
+ onClick: () => {
461
+ if (sessionId === null) return;
462
+ openTransientTab(planTab());
463
+ },
464
+ className: "min-w-0 gap-0.5 rounded-md px-1 py-1 hover:bg-doom-panel",
465
+ children: /* @__PURE__ */ jsxs("span", {
466
+ className: "flex w-full min-w-0 items-center gap-1.5",
467
+ children: [/* @__PURE__ */ jsx("span", {
468
+ "data-testid": "activity-plan-title",
469
+ className: "min-w-0 flex-1 truncate text-left text-base font-semibold text-doom-hi",
470
+ children: view.title
471
+ }), view.stamp === "" ? null : /* @__PURE__ */ jsx("span", {
472
+ "data-testid": "activity-plan-stamp",
473
+ className: "shrink-0 text-sm tabular-nums text-doom-dim",
474
+ children: view.stamp
475
+ })]
476
+ })
477
+ }), /* @__PURE__ */ jsx("p", {
478
+ className: "px-1 text-sm leading-relaxed text-doom-dim",
479
+ children: "open to read or edit; the agent reads what you save."
480
+ })]
481
+ });
482
+ }
483
+ //#endregion
484
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/Plan.activity.plan.web.tsx
485
+ var Plan_activity_plan_web_default = defineFill({
486
+ slot: "activity.plan",
487
+ id: "plan",
488
+ component: PlanActivitySection
489
+ });
490
+ //#endregion
491
+ //#region src/extensions/workspaces/sessions/(frontend)/leader/toggle.web.ts
492
+ var toggle_web_default = defineLeaderBinding({
493
+ id: "plan.toggle",
494
+ path: [{
495
+ key: "p",
496
+ label: "plan",
497
+ detail: "read-only planning modes"
498
+ }, {
499
+ key: "e",
500
+ label: "toggle",
501
+ detail: "enter or leave read-only planning"
502
+ }],
503
+ command: "minor plan"
504
+ });
505
+ //#endregion
506
+ //#region src/extensions/workspaces/sessions/(frontend)/mode/plan/mode.web.ts
507
+ var mode_web_default = defineMinorModeFile({
508
+ name: "plan",
509
+ keys: "p e",
510
+ statusKey: "plan-mode",
511
+ order: 20
512
+ });
513
+ //#endregion
514
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/PlanReviewPrompt.tsx
515
+ /** Claims only the selector opened by complete_plan, leaving unrelated extension dialogs alone. */
516
+ function claimsPlanReviewPrompt(dialog) {
517
+ return dialog.method === "select" && dialog.title === "Plan complete. What would you like to do?" && dialog.options.length === PLAN_REVIEW_OPTIONS.length && PLAN_REVIEW_OPTIONS.every((option, index) => dialog.options[index] === option);
518
+ }
519
+ /** Keeps plan approval in the composer so the conversation remains visible while the user decides. */
520
+ function PlanReviewPrompt({ dialog, answer, cancel }) {
521
+ const [cursor, setCursor] = useState(0);
522
+ const frame = useRef(null);
523
+ useEffect(() => frame.current?.focus(), [dialog.id]);
524
+ return /* @__PURE__ */ jsxs("div", {
525
+ ref: frame,
526
+ role: "group",
527
+ tabIndex: -1,
528
+ "data-testid": "plan-review-prompt",
529
+ className: "flex min-w-0 flex-col overflow-hidden outline-none",
530
+ onKeyDown: (event) => {
531
+ if (event.key === "Escape") {
532
+ event.preventDefault();
533
+ event.stopPropagation();
534
+ cancel();
535
+ return;
536
+ }
537
+ handleOptionListKey(event, {
538
+ options: dialog.options,
539
+ cursor,
540
+ onCursorChange: setCursor,
541
+ onSelect: answer
542
+ });
543
+ },
544
+ children: [
545
+ /* @__PURE__ */ jsxs("div", {
546
+ className: "flex flex-col gap-1 px-3.5 pt-3 pb-2",
547
+ children: [
548
+ /* @__PURE__ */ jsx(SectionLabel, { children: "plan review" }),
549
+ /* @__PURE__ */ jsx("p", {
550
+ className: "text-base font-semibold leading-relaxed text-doom-hi",
551
+ children: dialog.title
552
+ }),
553
+ /* @__PURE__ */ jsx("p", {
554
+ className: "text-sm leading-relaxed text-doom-dim",
555
+ children: "Review the plan in the conversation before choosing what happens next."
556
+ })
557
+ ]
558
+ }),
559
+ /* @__PURE__ */ jsx(OptionList, {
560
+ options: dialog.options,
561
+ cursor,
562
+ onCursorChange: setCursor,
563
+ onSelect: answer,
564
+ testIdPrefix: "plan-review-option",
565
+ className: "max-h-48 px-3.5 pb-3"
566
+ }),
567
+ /* @__PURE__ */ jsxs("div", {
568
+ className: "flex min-h-[34px] items-center gap-2 border-t border-doom-border-soft px-3.5 py-2",
569
+ children: [/* @__PURE__ */ jsxs("span", {
570
+ className: "text-sm text-doom-dim",
571
+ children: [
572
+ optionListHint(dialog.options.length),
573
+ " · ",
574
+ /* @__PURE__ */ jsx(Kbd, { children: "esc" }),
575
+ " cancels"
576
+ ]
577
+ }), /* @__PURE__ */ jsx(Button, {
578
+ variant: "outline",
579
+ size: "sm",
580
+ "data-testid": "plan-review-cancel",
581
+ className: "ml-auto",
582
+ onClick: cancel,
583
+ children: "cancel"
584
+ })]
585
+ })
586
+ ]
587
+ });
588
+ }
589
+ //#endregion
590
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/planToolRender.ts
591
+ /**
592
+ * Purpose-first summaries for the plan-mode tools. The TUI leaves these on
593
+ * Pi's default shell, so the shapes here come straight from the tool
594
+ * definitions in src/services/planServerSession/index.ts: write_plan and complete_plan
595
+ * report through `details` (written, path, phase, exited), run_fable_plan
596
+ * carries the broker result, and record_debug_evidence only confirms.
597
+ */
598
+ const PLAN_TOOL_NAMES = [
599
+ "record_debug_evidence",
600
+ "run_fable_plan",
601
+ "write_plan",
602
+ "complete_plan"
603
+ ];
604
+ const COLLAPSED_BODY_LINES = 12;
605
+ const PREVIEW_LENGTH = 72;
606
+ const EVIDENCE_LISTS = [
607
+ ["logs", "log"],
608
+ ["correlatedTraceEvidence", "trace"],
609
+ ["processOutput", "process output"],
610
+ ["browserConsoleEvidence", "console entry"],
611
+ ["correlationIds", "correlation id"],
612
+ ["timestamps", "timestamp"],
613
+ ["verifiedFacts", "verified fact"],
614
+ ["hypotheses", "hypothesis"],
615
+ ["unavailableEvidence", "unavailable item"]
616
+ ];
617
+ const PACKET_LISTS = [
618
+ ["goal", "goal"],
619
+ ["constraints", "constraint"],
620
+ ["decisions", "decision"],
621
+ ["verifiedFindings", "verified finding"],
622
+ ["inferredFindings", "inferred finding"],
623
+ ["unresolvedQuestions", "open question"]
624
+ ];
625
+ function asRecord(value) {
626
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
627
+ }
628
+ function inlineText(value) {
629
+ let normalized = "";
630
+ for (const character of value) {
631
+ const code = character.codePointAt(0) ?? 0;
632
+ normalized += code <= 31 || code === 127 ? " " : character;
633
+ }
634
+ return normalized.replace(/\s+/gu, " ").trim();
635
+ }
636
+ function readString(record, key) {
637
+ const value = record?.[key];
638
+ return typeof value === "string" ? inlineText(value) || void 0 : void 0;
639
+ }
640
+ function readNumber(record, key) {
641
+ const value = record?.[key];
642
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
643
+ }
644
+ function listLength(record, key) {
645
+ const value = record[key];
646
+ return Array.isArray(value) ? value.length : 0;
647
+ }
648
+ function plural(count, singular) {
649
+ if (count === 1) return singular;
650
+ if (singular.endsWith("y")) return `${singular.slice(0, -1)}ies`;
651
+ return `${singular}s`;
652
+ }
653
+ function preview(value) {
654
+ if (!value) return void 0;
655
+ return value.length > PREVIEW_LENGTH ? `${value.slice(0, 69)}…` : value;
656
+ }
657
+ function countMetadata(args, lists) {
658
+ return lists.flatMap(([key, label]) => {
659
+ const count = listLength(args, key);
660
+ return count > 0 ? [`${count} ${plural(count, label)}`] : [];
661
+ });
662
+ }
663
+ function line(text, tone, extra = {}) {
664
+ return {
665
+ text,
666
+ tone,
667
+ ...extra
668
+ };
669
+ }
670
+ function outputText(result) {
671
+ if (!result) return "";
672
+ return result.content.flatMap((item) => {
673
+ const record = asRecord(item);
674
+ return record?.type === "text" && typeof record.text === "string" ? [record.text] : [];
675
+ }).join("\n");
676
+ }
677
+ function bodyLines(text, expanded) {
678
+ const all = text.split("\n").map((entry) => entry.trimEnd()).filter((entry) => entry.length > 0);
679
+ const shown = expanded ? all : all.slice(0, COLLAPSED_BODY_LINES);
680
+ const lines = shown.map((entry) => line(entry, "text"));
681
+ if (all.length > shown.length) lines.push(line(`… ${all.length - shown.length} more lines`, "dim"));
682
+ return lines;
683
+ }
684
+ /** The header beside the tool name: what the call is for and how much it carries. */
685
+ function planCallSummary(toolName, args) {
686
+ if (toolName === "record_debug_evidence") {
687
+ const issue = preview(readString(args, "issue"));
688
+ return {
689
+ action: "record evidence",
690
+ ...issue ? { detail: issue } : {},
691
+ metadata: countMetadata(args, EVIDENCE_LISTS)
692
+ };
693
+ }
694
+ if (toolName === "run_fable_plan") {
695
+ const metadata = countMetadata(args, PACKET_LISTS);
696
+ if (readString(args, "currentPlan")) metadata.push("with current plan");
697
+ return {
698
+ action: "fable draft",
699
+ metadata
700
+ };
701
+ }
702
+ if (toolName === "write_plan") return {
703
+ action: "save plan",
704
+ metadata: []
705
+ };
706
+ const decision = readString(args, "decision");
707
+ return {
708
+ action: decision ? `decide ${decision}` : "request approval",
709
+ metadata: []
710
+ };
711
+ }
712
+ function evidenceLines(args, result, options) {
713
+ const lines = [asRecord(result?.details)?.recorded === true ? line("✓ evidence recorded as planning context", "success") : line(`○ ${inlineText(outputText(result)) || "not recorded"}`, "muted")];
714
+ if (!options.expanded) return lines;
715
+ for (const [key, label] of [
716
+ ["issue", "issue"],
717
+ ["expectedBehavior", "expected"],
718
+ ["reproductionAttempt", "reproduction"],
719
+ ["actualBehavior", "actual"]
720
+ ]) {
721
+ const value = readString(args, key);
722
+ if (value) lines.push(line(`${label} ${value}`, "text"));
723
+ }
724
+ for (const [key, label] of EVIDENCE_LISTS) {
725
+ const raw = args[key];
726
+ const items = Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : [];
727
+ if (items.length === 0) continue;
728
+ lines.push(line(plural(items.length, label), "hi", { bold: true }));
729
+ lines.push(...items.map((item) => line(inlineText(item), "text", { indent: true })));
730
+ }
731
+ return lines;
732
+ }
733
+ function fableLines(result, options) {
734
+ const details = asRecord(result?.details);
735
+ const text = outputText(result);
736
+ if (details?.started !== true) {
737
+ const code = readString(details, "errorCode");
738
+ return [line(`○ ${inlineText(text) || "fable planning did not start"}${code ? ` · ${code}` : ""}`, "muted")];
739
+ }
740
+ const status = readString(details, "status") ?? "unknown";
741
+ const draft = typeof details?.draft === "string" ? details.draft : void 0;
742
+ const code = readString(details, "errorCode");
743
+ if (!draft) return [line(`✗ fable planning ${status}${code ? ` · ${code}` : ""}`, "error")];
744
+ const finished = status === "completed";
745
+ return [line(`${finished ? "✓" : "!"} fable draft · ${status}${code ? ` · ${code}` : ""}`, finished ? "success" : "warning", { bold: true }), ...bodyLines(draft, options.expanded)];
746
+ }
747
+ function writeLines(result, options) {
748
+ const details = asRecord(result?.details);
749
+ const path = readString(details, "path");
750
+ if (options.isPartial) return [line(`◐ ${readString(details, "phase") ?? "checking"}${path ? ` ${path}` : ""}`, "warning")];
751
+ const duration = readNumber(details, "durationMs");
752
+ if (details?.written === true) return [line(`✓ wrote ${path ?? "the plan"}${duration === void 0 ? "" : ` · ${duration}ms`}`, "success")];
753
+ const lines = [line(`○ ${inlineText(outputText(result)) || "plan not written"}`, "muted")];
754
+ if (path) lines.push(line(`path ${path}`, "dim"));
755
+ return lines;
756
+ }
757
+ function completeLines(result) {
758
+ const text = inlineText(outputText(result));
759
+ if (asRecord(result?.details)?.exited === true) return [line(`✓ plan mode exited${text ? ` · ${text}` : ""}`, "success")];
760
+ return [line(`○ staying in plan mode${text ? ` · ${text}` : ""}`, "muted")];
761
+ }
762
+ /** The body of a plan tool card: the outcome line, plus the carried detail once expanded. */
763
+ function planResultLines(toolName, args, result, options) {
764
+ if (options.isError) return [line(`✗ ${inlineText(outputText(result)) || `${toolName} failed`}`, "error")];
765
+ if (toolName === "record_debug_evidence") return evidenceLines(args, result, options);
766
+ if (toolName === "run_fable_plan") {
767
+ if (options.isPartial) return [line("◐ drafting with the local Fable broker", "warning")];
768
+ return fableLines(result, options);
769
+ }
770
+ if (toolName === "write_plan") return writeLines(result, options);
771
+ if (options.isPartial) return [line("◐ waiting for the review decision", "warning")];
772
+ return completeLines(result);
773
+ }
774
+ //#endregion
775
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/PlanToolMessage.tsx
776
+ /**
777
+ * The plan tools' timeline item: the plan action, the issue or decision it
778
+ * targets, and what the packet carries in the header; the outcome line, with
779
+ * the recorded evidence or the Fable draft once expanded, in the body.
780
+ */
781
+ function PlanToolMessage({ toolName, args, result, running, isError }) {
782
+ const summary = planCallSummary(toolName, args);
783
+ const collapsed = planResultLines(toolName, args, result, {
784
+ expanded: false,
785
+ isError,
786
+ isPartial: running
787
+ });
788
+ const full = planResultLines(toolName, args, result, {
789
+ expanded: true,
790
+ isError,
791
+ isPartial: running
792
+ });
793
+ return /* @__PURE__ */ jsx(MessageItem, {
794
+ tone: toolTone({
795
+ running,
796
+ isError
797
+ }),
798
+ expandable: full.length > collapsed.length,
799
+ children: ({ expanded }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
800
+ title: summary.action,
801
+ children: /* @__PURE__ */ jsx("span", {
802
+ "data-testid": `tool-call-${toolName}`,
803
+ title: [summary.detail, ...summary.metadata].filter(Boolean).join(" · ") || void 0,
804
+ className: "min-w-0 flex-1 truncate text-doom-dim",
805
+ children: [summary.detail, ...summary.metadata].filter(Boolean).join(" · ")
806
+ })
807
+ }), (expanded ? full : collapsed).length > 0 ? /* @__PURE__ */ jsx(MessageItemBody, {
808
+ "data-testid": `tool-result-${toolName}`,
809
+ children: /* @__PURE__ */ jsx(MessageLines, { lines: expanded ? full : collapsed })
810
+ }) : null] })
811
+ });
812
+ }
813
+ //#endregion
814
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/plan.web.tsx
815
+ var plan_web_default = defineToolRenderer({
816
+ tools: [...PLAN_TOOL_NAMES],
817
+ message: PlanToolMessage,
818
+ prompt: {
819
+ claims: claimsPlanReviewPrompt,
820
+ component: PlanReviewPrompt
821
+ }
822
+ });
823
+ //#endregion
824
+ //#region generated/web.ts
825
+ const via = (identity, value) => value === void 0 ? void 0 : {
826
+ ...identity,
827
+ ...value
828
+ };
829
+ const webPlugin = defineWebPlugin({
830
+ id: "plan",
831
+ global: { get settingsSections() {
832
+ return [via({ id: "plan" }, plan_web_default$3)];
833
+ } },
834
+ workspace: { get settingsSections() {
835
+ return [via({ id: "plan" }, plan_web_default$2)];
836
+ } },
837
+ session: {
838
+ get activityGroups() {
839
+ return [via({ name: "plan" }, plan_web_default$1)];
840
+ },
841
+ get fills() {
842
+ return [via({
843
+ slot: "activity.plan",
844
+ id: "plan"
845
+ }, Plan_activity_plan_web_default)];
846
+ },
847
+ get leaderBindings() {
848
+ return [via({ id: "plan.toggle" }, toggle_web_default)];
849
+ },
850
+ get minorModes() {
851
+ return [via({ name: "plan" }, mode_web_default)];
852
+ },
853
+ get toolRenderers() {
854
+ return [via({ tools: ["plan"] }, plan_web_default)];
855
+ }
856
+ }
857
+ });
858
+ //#endregion
859
+ export { webPlugin };
860
+
861
+ //# sourceMappingURL=web.mjs.map