opencode-planpilot 0.2.3 → 0.3.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.
@@ -0,0 +1,704 @@
1
+ // src/studio-web/planpilot-todo-bar.ts
2
+ var UI_STRINGS = {
3
+ "en-US": {
4
+ viewingSuffix: "(viewing)",
5
+ loading: "Loading...",
6
+ noPlans: "No plans",
7
+ checkingPlanStatus: "Checking plan status...",
8
+ noPlansFoundForSession: "No plans found for this session yet.",
9
+ noSelectedPlanHint: "No plan is selected right now. Open Plan List to review a recent plan.",
10
+ planDetailUnavailable: "Plan detail is unavailable",
11
+ showPlan: "Show plan",
12
+ noPlansInSession: "No plans in this session.",
13
+ active: "Active",
14
+ noStepsYet: "This plan has no steps yet.",
15
+ waiting: "Waiting",
16
+ paused: "Paused",
17
+ updating: "Updating...",
18
+ refresh: "Refresh",
19
+ planList: "Plan List",
20
+ close: "Close",
21
+ collapse: "Collapse",
22
+ error: "Error"
23
+ },
24
+ "zh-CN": {
25
+ viewingSuffix: "(\u67E5\u770B\u4E2D)",
26
+ loading: "\u52A0\u8F7D\u4E2D...",
27
+ noPlans: "\u6682\u65E0\u8BA1\u5212",
28
+ checkingPlanStatus: "\u6B63\u5728\u68C0\u67E5\u8BA1\u5212\u72B6\u6001...",
29
+ noPlansFoundForSession: "\u6B64\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u8BA1\u5212\u3002",
30
+ noSelectedPlanHint: "\u5F53\u524D\u672A\u9009\u62E9\u8BA1\u5212\u3002\u6253\u5F00\u8BA1\u5212\u5217\u8868\u67E5\u770B\u6700\u8FD1\u7684\u8BA1\u5212\u3002",
31
+ planDetailUnavailable: "\u8BA1\u5212\u8BE6\u60C5\u4E0D\u53EF\u7528",
32
+ showPlan: "\u663E\u793A\u8BA1\u5212",
33
+ noPlansInSession: "\u6B64\u4F1A\u8BDD\u4E2D\u6682\u65E0\u8BA1\u5212\u3002",
34
+ active: "\u8FDB\u884C\u4E2D",
35
+ noStepsYet: "\u8BE5\u8BA1\u5212\u8FD8\u6CA1\u6709\u6B65\u9AA4\u3002",
36
+ waiting: "\u7B49\u5F85\u4E2D",
37
+ paused: "\u5DF2\u6682\u505C",
38
+ updating: "\u66F4\u65B0\u4E2D...",
39
+ refresh: "\u5237\u65B0",
40
+ planList: "\u8BA1\u5212\u5217\u8868",
41
+ close: "\u5173\u95ED",
42
+ collapse: "\u6536\u8D77",
43
+ error: "\u9519\u8BEF"
44
+ }
45
+ };
46
+ function normalizeLocale(value) {
47
+ const normalized = String(value || "").trim().toLowerCase();
48
+ if (!normalized) return "en-US";
49
+ if (normalized.startsWith("zh")) return "zh-CN";
50
+ return "en-US";
51
+ }
52
+ function asObject(value) {
53
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
54
+ return value;
55
+ }
56
+ function asArray(value) {
57
+ return Array.isArray(value) ? value : [];
58
+ }
59
+ function toNumber(value, fallback = 0) {
60
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
61
+ return Math.trunc(value);
62
+ }
63
+ function toStringValue(value, fallback = "") {
64
+ if (typeof value !== "string") return fallback;
65
+ const trimmed = value.trim();
66
+ return trimmed || fallback;
67
+ }
68
+ function parseGoal(value) {
69
+ const obj = asObject(value);
70
+ const id = toNumber(obj.id);
71
+ const stepId = toNumber(obj.step_id);
72
+ const content = toStringValue(obj.content);
73
+ if (!id || !stepId || !content) return null;
74
+ return {
75
+ id,
76
+ step_id: stepId,
77
+ content,
78
+ status: toStringValue(obj.status) === "done" ? "done" : "todo"
79
+ };
80
+ }
81
+ function parseStep(value) {
82
+ const obj = asObject(value);
83
+ const id = toNumber(obj.id);
84
+ const planId = toNumber(obj.plan_id);
85
+ const content = toStringValue(obj.content);
86
+ if (!id || !planId || !content) return null;
87
+ return {
88
+ id,
89
+ plan_id: planId,
90
+ content,
91
+ status: toStringValue(obj.status) === "done" ? "done" : "todo",
92
+ executor: toStringValue(obj.executor) === "human" ? "human" : "ai",
93
+ sort_order: toNumber(obj.sort_order),
94
+ comment: typeof obj.comment === "string" ? obj.comment : null
95
+ };
96
+ }
97
+ function parsePlan(value) {
98
+ const obj = asObject(value);
99
+ const id = toNumber(obj.id);
100
+ const title = toStringValue(obj.title);
101
+ if (!id || !title) return null;
102
+ return {
103
+ id,
104
+ title,
105
+ content: toStringValue(obj.content),
106
+ status: toStringValue(obj.status) === "done" ? "done" : "todo",
107
+ comment: typeof obj.comment === "string" ? obj.comment : null,
108
+ last_session_id: typeof obj.last_session_id === "string" ? obj.last_session_id : null,
109
+ updated_at: toNumber(obj.updated_at)
110
+ };
111
+ }
112
+ function parseRuntime(value) {
113
+ const obj = asObject(value);
114
+ const activeObj = asObject(obj.activePlan);
115
+ const nextObj = asObject(obj.nextStep);
116
+ const nextStepRow = parseStep(nextObj.step);
117
+ const nextGoals = asArray(nextObj.goals).map(parseGoal).filter((goal) => !!goal);
118
+ const waitObj = asObject(nextObj.wait);
119
+ const nextStepDetail = nextStepRow ? {
120
+ step: nextStepRow,
121
+ goals: nextGoals,
122
+ wait: waitObj.until ? {
123
+ until: toNumber(waitObj.until),
124
+ reason: toStringValue(waitObj.reason) || void 0
125
+ } : null
126
+ } : null;
127
+ return {
128
+ paused: obj.paused === true,
129
+ activePlan: activeObj.plan_id ? { plan_id: toNumber(activeObj.plan_id) } : null,
130
+ nextStep: nextStepDetail
131
+ };
132
+ }
133
+ function parsePlanDetail(value) {
134
+ const obj = asObject(value);
135
+ const plan = parsePlan(obj.plan);
136
+ if (!plan) return null;
137
+ const steps = asArray(obj.steps).map(parseStep).filter((step) => !!step);
138
+ const goals = asArray(obj.goals).map((entry) => {
139
+ const e = asObject(entry);
140
+ const stepId = toNumber(e.stepId);
141
+ if (!stepId) return null;
142
+ const list = asArray(e.goals).map(parseGoal).filter((goal) => !!goal);
143
+ return { stepId, goals: list };
144
+ }).filter((entry) => !!entry);
145
+ return { plan, steps, goals };
146
+ }
147
+ function formattedWait(stepDetail, locale) {
148
+ const wait = stepDetail?.wait;
149
+ if (!wait?.until) return "";
150
+ const when = new Date(wait.until);
151
+ if (Number.isNaN(when.getTime())) return "";
152
+ const time = when.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
153
+ return wait.reason ? `${time} (${wait.reason})` : time;
154
+ }
155
+ function summarizePlanContent(content) {
156
+ const normalized = String(content || "").replace(/\s+/g, " ").trim();
157
+ if (!normalized) return "";
158
+ if (normalized.length <= 92) return normalized;
159
+ return `${normalized.slice(0, 92)}...`;
160
+ }
161
+ function htmlEscape(value) {
162
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
163
+ }
164
+ function iconSvg(name, className) {
165
+ const common = `class="${className}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"`;
166
+ if (name === "check") {
167
+ return `<svg ${common}><path d="M20 6 9 17l-5-5"/></svg>`;
168
+ }
169
+ if (name === "chev") {
170
+ return `<svg ${common}><path d="m6 9 6 6 6-6"/></svg>`;
171
+ }
172
+ if (name === "refresh") {
173
+ return `<svg ${common}><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>`;
174
+ }
175
+ if (name === "list") {
176
+ return `<svg ${common}><path d="M8 6h13"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M3 6h.01"/><path d="M3 12h.01"/><path d="M3 18h.01"/></svg>`;
177
+ }
178
+ if (name === "hide") {
179
+ return `<svg ${common}><path d="M3 3l18 18"/><path d="M10.58 10.58A3 3 0 0 0 12 15a3 3 0 0 0 2.42-4.42"/><path d="M9.88 5.09A10.4 10.4 0 0 1 12 5c7 0 10 7 10 7a18 18 0 0 1-3.2 4.2"/><path d="M6.1 6.1A18.5 18.5 0 0 0 2 12s3 7 10 7a10.7 10.7 0 0 0 3.1-.4"/></svg>`;
180
+ }
181
+ if (name === "x") {
182
+ return `<svg ${common}><path d="m6 6 12 12"/><path d="m18 6-12 12"/></svg>`;
183
+ }
184
+ if (name === "clock") {
185
+ return `<svg ${common}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>`;
186
+ }
187
+ if (name === "stack") {
188
+ return `<svg ${common}><path d="M12 2 2 7l10 5 10-5-10-5Z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/></svg>`;
189
+ }
190
+ return "";
191
+ }
192
+ function mount(el, opts) {
193
+ const sessionId = String(opts.context?.sessionId || "").trim();
194
+ const locale = normalizeLocale(opts.context?.locale || opts.context?.lang);
195
+ const t = UI_STRINGS[locale];
196
+ const hostManagedMode = opts.context?.studioOverlayMode === "host-menu";
197
+ const state = {
198
+ sessionId,
199
+ loading: false,
200
+ busy: false,
201
+ error: null,
202
+ showOtherPlans: false,
203
+ collapsed: !hostManagedMode,
204
+ viewedPlanId: 0,
205
+ goalsExpandedByStepId: {},
206
+ runtime: null,
207
+ activePlanDetail: null,
208
+ sessionPlans: []
209
+ };
210
+ let refreshTimer = null;
211
+ let stopEvents = null;
212
+ let reserveObserver = null;
213
+ let reserveRaf = 0;
214
+ function setReservePx(px) {
215
+ if (!opts.layout) return;
216
+ opts.layout.setReservePx(px);
217
+ }
218
+ function computeReserve() {
219
+ const rect = el.getBoundingClientRect();
220
+ if (!Number.isFinite(rect.height) || rect.height <= 0) return 0;
221
+ const bottomGap = 8;
222
+ return Math.max(0, Math.ceil(rect.height + bottomGap));
223
+ }
224
+ function scheduleReserveUpdate() {
225
+ if (!opts.layout) return;
226
+ if (reserveRaf) return;
227
+ reserveRaf = window.requestAnimationFrame(() => {
228
+ reserveRaf = 0;
229
+ if (!state.sessionId) {
230
+ setReservePx(0);
231
+ return;
232
+ }
233
+ setReservePx(computeReserve());
234
+ });
235
+ }
236
+ function isVisible() {
237
+ return Boolean(state.sessionId);
238
+ }
239
+ function activePlan() {
240
+ return state.activePlanDetail?.plan ?? null;
241
+ }
242
+ function activeRuntimePlanId() {
243
+ return state.runtime?.activePlan?.plan_id ?? 0;
244
+ }
245
+ function activePlanId() {
246
+ return activePlan()?.id ?? 0;
247
+ }
248
+ function orderedSteps(detail) {
249
+ return [...detail.steps].sort((a, b) => {
250
+ if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order;
251
+ return a.id - b.id;
252
+ });
253
+ }
254
+ function goalsForStepId(detail, stepId) {
255
+ const entry = detail.goals.find((g) => g.stepId === stepId);
256
+ const list = entry?.goals ?? [];
257
+ return [...list].sort((a, b) => a.id - b.id);
258
+ }
259
+ function isGoalsExpanded(stepId) {
260
+ return state.goalsExpandedByStepId[String(stepId)] === true;
261
+ }
262
+ function toggleGoalsExpanded(stepId) {
263
+ const stepsScrollEl = el.querySelector('[data-pp-scroll="steps"]');
264
+ const prevStepsScrollTop = stepsScrollEl ? stepsScrollEl.scrollTop : null;
265
+ const key = String(stepId);
266
+ state.goalsExpandedByStepId = {
267
+ ...state.goalsExpandedByStepId,
268
+ [key]: !(state.goalsExpandedByStepId[key] === true)
269
+ };
270
+ render();
271
+ if (prevStepsScrollTop !== null) {
272
+ const restore = () => {
273
+ const nextStepsScrollEl = el.querySelector('[data-pp-scroll="steps"]');
274
+ if (nextStepsScrollEl) nextStepsScrollEl.scrollTop = prevStepsScrollTop;
275
+ };
276
+ restore();
277
+ window.requestAnimationFrame(restore);
278
+ }
279
+ scheduleReserveUpdate();
280
+ }
281
+ function headerLabel() {
282
+ const plan = activePlan();
283
+ if (plan) return `#${plan.id} ${plan.title}`;
284
+ if (state.activePlanDetail?.plan) {
285
+ const p = state.activePlanDetail.plan;
286
+ return `#${p.id} ${p.title} ${t.viewingSuffix}`;
287
+ }
288
+ if (state.loading) return t.loading;
289
+ return t.noPlans;
290
+ }
291
+ function planContent() {
292
+ const content = state.activePlanDetail?.plan.content;
293
+ return String(content || "").trim();
294
+ }
295
+ function fallbackStatusMessage() {
296
+ if (state.loading && !state.activePlanDetail) return t.checkingPlanStatus;
297
+ if (!state.activePlanDetail) {
298
+ if (state.sessionPlans.length === 0) {
299
+ return t.noPlansFoundForSession;
300
+ }
301
+ return t.noSelectedPlanHint;
302
+ }
303
+ return "";
304
+ }
305
+ async function invoke(action, payload = null) {
306
+ return await opts.host.invokeAction(action, payload, null);
307
+ }
308
+ async function refreshAll() {
309
+ if (!isVisible()) {
310
+ state.runtime = null;
311
+ state.activePlanDetail = null;
312
+ state.sessionPlans = [];
313
+ state.error = null;
314
+ state.loading = false;
315
+ render();
316
+ setReservePx(0);
317
+ return;
318
+ }
319
+ state.loading = true;
320
+ state.error = null;
321
+ render();
322
+ scheduleReserveUpdate();
323
+ try {
324
+ const [runtimeRaw, activeRaw, plansRaw] = await Promise.all([
325
+ invoke("runtime.snapshot"),
326
+ invoke("plan.active"),
327
+ invoke("plan.list", {})
328
+ ]);
329
+ state.runtime = parseRuntime(runtimeRaw);
330
+ const activeObj = asObject(activeRaw);
331
+ const activeDetail = parsePlanDetail(activeObj.detail);
332
+ const parsedPlans = asArray(plansRaw).map(parsePlan).filter((plan) => !!plan).filter((plan) => plan.last_session_id === state.sessionId).sort((a, b) => {
333
+ if (a.updated_at !== b.updated_at) return b.updated_at - a.updated_at;
334
+ return b.id - a.id;
335
+ });
336
+ state.sessionPlans = parsedPlans;
337
+ const planIdSet = new Set(parsedPlans.map((p) => p.id));
338
+ if (state.viewedPlanId > 0 && !planIdSet.has(state.viewedPlanId)) {
339
+ state.viewedPlanId = 0;
340
+ }
341
+ const fallbackRecentPlanId = parsedPlans[0]?.id ?? 0;
342
+ const targetPlanId = state.viewedPlanId > 0 ? state.viewedPlanId : (activeDetail?.plan?.id ?? 0) || fallbackRecentPlanId;
343
+ if (!targetPlanId) {
344
+ state.activePlanDetail = null;
345
+ } else if (activeDetail && activeDetail.plan.id === targetPlanId) {
346
+ state.activePlanDetail = activeDetail;
347
+ } else {
348
+ const detailRaw = await invoke("plan.get", { id: targetPlanId });
349
+ state.activePlanDetail = parsePlanDetail(detailRaw);
350
+ }
351
+ } catch (error) {
352
+ state.error = error instanceof Error ? error.message : String(error);
353
+ state.runtime = null;
354
+ state.activePlanDetail = null;
355
+ state.sessionPlans = [];
356
+ } finally {
357
+ state.loading = false;
358
+ render();
359
+ scheduleReserveUpdate();
360
+ }
361
+ }
362
+ function scheduleRefresh(delayMs = 120) {
363
+ if (refreshTimer) window.clearTimeout(refreshTimer);
364
+ refreshTimer = window.setTimeout(() => {
365
+ refreshTimer = null;
366
+ void refreshAll();
367
+ }, Math.max(0, Math.floor(delayMs)));
368
+ }
369
+ function toggleCollapsed() {
370
+ if (hostManagedMode) return;
371
+ state.collapsed = !state.collapsed;
372
+ if (state.collapsed) {
373
+ state.showOtherPlans = false;
374
+ render();
375
+ scheduleReserveUpdate();
376
+ } else {
377
+ void refreshAll();
378
+ }
379
+ }
380
+ async function openPlan(planId) {
381
+ if (!planId || state.busy) return;
382
+ state.busy = true;
383
+ state.error = null;
384
+ render();
385
+ scheduleReserveUpdate();
386
+ try {
387
+ state.showOtherPlans = false;
388
+ const detailRaw = await invoke("plan.get", { id: planId });
389
+ const detail = parsePlanDetail(detailRaw);
390
+ if (!detail) throw new Error(t.planDetailUnavailable);
391
+ state.viewedPlanId = planId;
392
+ state.activePlanDetail = detail;
393
+ } catch (error) {
394
+ state.error = error instanceof Error ? error.message : String(error);
395
+ } finally {
396
+ state.busy = false;
397
+ render();
398
+ scheduleReserveUpdate();
399
+ }
400
+ }
401
+ function renderCollapsedButton() {
402
+ return `
403
+ <div class="pointer-events-auto p-1">
404
+ <button
405
+ type="button"
406
+ data-pp-action="toggle"
407
+ class="h-9 w-9 rounded-full shadow-md border border-border/50 bg-background/80 backdrop-blur hover:bg-background transition-all inline-flex items-center justify-center"
408
+ aria-label="${t.showPlan}"
409
+ title="${t.showPlan}"
410
+ ${state.busy ? "disabled" : ""}
411
+ >
412
+ ${iconSvg("list", "h-5 w-5 text-muted-foreground")}
413
+ </button>
414
+ </div>
415
+ `;
416
+ }
417
+ function renderPlanList() {
418
+ if (state.sessionPlans.length === 0) {
419
+ return `<div class="px-2 py-2 text-center text-xs text-muted-foreground">${t.noPlansInSession}</div>`;
420
+ }
421
+ const activeId = activePlanId();
422
+ const runtimeActiveId = activeRuntimePlanId();
423
+ const rows = state.sessionPlans.map((plan) => {
424
+ const isActive = plan.id === activeId;
425
+ const isRuntimeActive = plan.id === runtimeActiveId;
426
+ const title = summarizePlanContent(plan.content) || `#${plan.id} ${plan.title}`;
427
+ const badge = isRuntimeActive ? `<span class="shrink-0 text-[10px] px-1 rounded bg-emerald-500/15 text-emerald-700">${t.active}</span>` : "";
428
+ const statusIcon = plan.status === "done" ? iconSvg("check", "h-4 w-4 shrink-0 text-emerald-700/70") : iconSvg("stack", "h-4 w-4 shrink-0 text-muted-foreground/70");
429
+ return `
430
+ <button
431
+ type="button"
432
+ data-pp-plan-open="${plan.id}"
433
+ class="w-full h-8 text-left flex items-center gap-2 rounded-md border border-transparent px-2 transition-colors ${isActive ? "bg-primary/5 border-primary/15" : "hover:bg-muted/30"}"
434
+ ${state.busy ? "disabled" : ""}
435
+ title="${htmlEscape(title)}"
436
+ >
437
+ <div class="min-w-0 flex-1 flex items-center gap-1.5">
438
+ <span class="min-w-0 text-xs font-medium truncate">#${plan.id} ${htmlEscape(plan.title)}</span>
439
+ ${badge}
440
+ </div>
441
+ ${statusIcon}
442
+ </button>
443
+ `;
444
+ }).join("");
445
+ return `<div class="overflow-y-auto overscroll-contain flex-1 min-h-0" style="max-height: 170px"><div class="flex flex-col gap-[2px]">${rows}</div></div>`;
446
+ }
447
+ function renderSteps(detail) {
448
+ const steps = orderedSteps(detail);
449
+ if (!steps.length) {
450
+ return `<div class="py-2 text-center text-xs text-muted-foreground italic leading-relaxed">${t.noStepsYet}</div>`;
451
+ }
452
+ const runtimeNextStepId = state.runtime?.activePlan?.plan_id === detail.plan.id ? state.runtime?.nextStep?.step.id ?? 0 : 0;
453
+ const derivedNextStepId = runtimeNextStepId ? 0 : steps.filter((step) => step.status !== "done").sort((a, b) => {
454
+ if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order;
455
+ return a.id - b.id;
456
+ })[0]?.id ?? 0;
457
+ const nextStepId = runtimeNextStepId || derivedNextStepId;
458
+ const nextWaitLabel = runtimeNextStepId ? formattedWait(state.runtime?.nextStep ?? null, locale) : "";
459
+ const items = steps.map((step) => {
460
+ const goals = goalsForStepId(detail, step.id);
461
+ const isNext = step.id === nextStepId;
462
+ const rowClass = isNext ? "bg-primary/5 border-primary/15" : step.status === "done" ? "bg-muted/20 border-border/30 hover:bg-muted/25" : "hover:bg-muted/30";
463
+ const waitIcon = isNext && nextWaitLabel ? `<span title="${htmlEscape(`${t.waiting}: ${nextWaitLabel}`)}" aria-label="${t.waiting}">${iconSvg(
464
+ "clock",
465
+ "h-4 w-4 text-amber-600/70"
466
+ )}</span>` : "";
467
+ const doneIcon = step.status === "done" ? iconSvg("check", "h-4 w-4 text-emerald-700/70") : "";
468
+ const arrow = goals.length ? `<span class="${isGoalsExpanded(step.id) ? "" : "-rotate-90"} transition-transform">${iconSvg(
469
+ "chev",
470
+ "h-4 w-4 text-muted-foreground/60"
471
+ )}</span>` : "";
472
+ const goalsBlock = goals.length && isGoalsExpanded(step.id) ? `
473
+ <div class="pb-1 pl-7 pr-1.5">
474
+ <div class="space-y-0.5">
475
+ ${goals.map((goal) => {
476
+ const line = goal.status === "done" ? "bg-emerald-600/40" : "bg-muted-foreground/35";
477
+ const goalDone = goal.status === "done" ? iconSvg("check", "mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-700/70") : "";
478
+ return `
479
+ <div class="flex items-start gap-2">
480
+ <span class="mt-2 h-px w-2 shrink-0 ${line}" aria-hidden="true"></span>
481
+ <span class="min-w-0 flex-1 text-[11px] text-muted-foreground leading-snug break-words whitespace-pre-wrap ${goal.status === "done" ? "opacity-75" : ""}">${htmlEscape(goal.content)}</span>
482
+ ${goalDone}
483
+ </div>
484
+ `;
485
+ }).join("")}
486
+ </div>
487
+ </div>
488
+ ` : "";
489
+ const clickableAttrs = goals.length ? `data-pp-step-toggle="${step.id}" role="button" tabindex="0" aria-expanded="${isGoalsExpanded(step.id)}"` : "";
490
+ return `
491
+ <li class="rounded-md border border-transparent transition-colors ${rowClass}">
492
+ <div
493
+ class="flex items-center gap-2 h-9 px-1.5 ${goals.length ? "cursor-pointer" : ""}"
494
+ ${clickableAttrs}
495
+ title="${htmlEscape(step.content)}"
496
+ >
497
+ <div class="min-w-0 flex-1">
498
+ <div class="text-[13px] leading-[1.1] truncate ${step.status === "done" ? "text-muted-foreground opacity-75" : "text-foreground"}">
499
+ ${htmlEscape(step.content)}
500
+ </div>
501
+ </div>
502
+ <div class="flex items-center gap-1 shrink-0">
503
+ ${waitIcon}
504
+ ${doneIcon}
505
+ ${arrow}
506
+ </div>
507
+ </div>
508
+ ${goalsBlock}
509
+ </li>
510
+ `;
511
+ }).join("");
512
+ return `<div data-pp-scroll="steps" class="overflow-y-auto overscroll-contain flex-1 min-h-0" style="max-height: 200px"><ol class="flex flex-col gap-[2px]">${items}</ol></div>`;
513
+ }
514
+ function renderExpandedPanel() {
515
+ const detail = state.activePlanDetail;
516
+ const content = planContent();
517
+ const paused = state.runtime?.paused === true;
518
+ const main = detail === null ? `<div class="py-2 text-center text-xs text-muted-foreground italic leading-relaxed">${htmlEscape(
519
+ fallbackStatusMessage()
520
+ )}</div>` : state.showOtherPlans ? `
521
+ <div class="flex flex-col gap-1 min-h-0">
522
+ ${renderPlanList()}
523
+ </div>
524
+ ` : `
525
+ <div class="flex flex-col gap-1.5 min-h-0">
526
+ ${content ? `<div class="text-[10px] text-muted-foreground leading-snug whitespace-pre-wrap break-words max-h-12 overflow-hidden" title="${htmlEscape(
527
+ content
528
+ )}">${htmlEscape(content)}</div>` : ""}
529
+ ${paused ? `<div class="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground"><span class="inline-flex items-center rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">${t.paused}</span></div>` : ""}
530
+ ${renderSteps(detail)}
531
+ </div>
532
+ `;
533
+ const errorBlock = state.error ? `<div class="rounded bg-destructive/10 px-2 py-1.5 text-xs text-destructive flex items-start gap-2"><span class="font-bold">${t.error}:</span> ${htmlEscape(
534
+ state.error
535
+ )}</div>` : "";
536
+ const header = `
537
+ <div class="flex items-center border-b border-border/30 bg-muted/20 gap-2 px-2 py-1">
538
+ <div class="flex items-center gap-1.5 min-w-0 flex-1">
539
+ <span class="truncate text-[11px] font-semibold text-foreground/90 select-none cursor-default" title="${htmlEscape(
540
+ headerLabel()
541
+ )}">${htmlEscape(headerLabel())}</span>
542
+ ${detail && detail.plan.status === "done" ? iconSvg("check", "h-3.5 w-3.5 shrink-0 text-emerald-700/70") : ""}
543
+ ${state.loading ? `<span class="animate-pulse text-[10px] text-muted-foreground">${t.updating}</span>` : ""}
544
+ </div>
545
+ <div class="flex items-center gap-0.5">
546
+ <button
547
+ type="button"
548
+ data-pp-action="refresh"
549
+ class="h-7 w-7 inline-flex items-center justify-center rounded hover:bg-muted/40"
550
+ title="${t.refresh}"
551
+ aria-label="${t.refresh}"
552
+ ${state.busy ? "disabled" : ""}
553
+ >
554
+ ${iconSvg("refresh", "h-3.5 w-3.5 text-muted-foreground/70")}
555
+ </button>
556
+ <button
557
+ type="button"
558
+ data-pp-action="toggleList"
559
+ class="h-7 w-7 inline-flex items-center justify-center rounded ${state.showOtherPlans ? "bg-muted/40" : "hover:bg-muted/40"}"
560
+ title="${t.planList}"
561
+ aria-label="${t.planList}"
562
+ aria-pressed="${state.showOtherPlans}"
563
+ >
564
+ ${iconSvg("list", "h-3.5 w-3.5")}
565
+ </button>
566
+ ${hostManagedMode ? `<div class="mx-1 h-3 w-px bg-border/50"></div>
567
+ <button
568
+ type="button"
569
+ data-pp-action="close"
570
+ class="h-7 w-7 inline-flex items-center justify-center rounded hover:bg-muted/40"
571
+ title="${t.close}"
572
+ aria-label="${t.close}"
573
+ >
574
+ ${iconSvg("x", "h-3.5 w-3.5 text-muted-foreground/70")}
575
+ </button>` : `<div class="mx-1 h-3 w-px bg-border/50"></div>
576
+ <button
577
+ type="button"
578
+ data-pp-action="toggle"
579
+ class="h-7 w-7 inline-flex items-center justify-center rounded hover:bg-muted/40"
580
+ title="${t.collapse}"
581
+ aria-label="${t.collapse}"
582
+ >
583
+ ${iconSvg("hide", "h-3.5 w-3.5 text-muted-foreground/70")}
584
+ </button>`}
585
+ </div>
586
+ </div>
587
+ `;
588
+ return `
589
+ <section class="pointer-events-auto w-full rounded-lg border border-border/60 bg-background/95 shadow-xl backdrop-blur-md overflow-hidden transition-all duration-300 ease-in-out flex flex-col max-h-[50vh]">
590
+ ${header}
591
+ <div class="overflow-hidden flex-1 min-h-0 flex flex-col p-1.5 gap-1.5">
592
+ ${main}
593
+ ${errorBlock}
594
+ </div>
595
+ </section>
596
+ `;
597
+ }
598
+ function render() {
599
+ if (!isVisible()) {
600
+ el.innerHTML = "";
601
+ setReservePx(0);
602
+ return;
603
+ }
604
+ const body = hostManagedMode ? renderExpandedPanel() : state.collapsed ? renderCollapsedButton() : renderExpandedPanel();
605
+ const containerClass = hostManagedMode ? "pointer-events-none w-full" : "pointer-events-none w-full flex justify-end";
606
+ el.innerHTML = `<div class="${containerClass}">${body}</div>`;
607
+ }
608
+ function handleClick(event) {
609
+ const target = event.target;
610
+ if (!target) return;
611
+ const actionEl = target.closest("[data-pp-action]");
612
+ if (actionEl) {
613
+ const action = String(actionEl.dataset.ppAction || "").trim();
614
+ if (action === "toggle") {
615
+ toggleCollapsed();
616
+ return;
617
+ }
618
+ if (action === "toggleList") {
619
+ state.showOtherPlans = !state.showOtherPlans;
620
+ render();
621
+ scheduleReserveUpdate();
622
+ return;
623
+ }
624
+ if (action === "refresh") {
625
+ scheduleRefresh(0);
626
+ return;
627
+ }
628
+ if (action === "close") {
629
+ opts.close?.();
630
+ return;
631
+ }
632
+ return;
633
+ }
634
+ const planEl = target.closest("[data-pp-plan-open]");
635
+ if (planEl) {
636
+ const id = Number(planEl.dataset.ppPlanOpen);
637
+ if (Number.isFinite(id) && id > 0) {
638
+ void openPlan(Math.trunc(id));
639
+ }
640
+ return;
641
+ }
642
+ const stepEl = target.closest("[data-pp-step-toggle]");
643
+ if (stepEl) {
644
+ const id = Number(stepEl.dataset.ppStepToggle);
645
+ if (Number.isFinite(id) && id > 0) {
646
+ toggleGoalsExpanded(Math.trunc(id));
647
+ }
648
+ }
649
+ }
650
+ function handleKeydown(event) {
651
+ const target = event.target;
652
+ if (!target) return;
653
+ if (event.key !== "Enter" && event.key !== " ") return;
654
+ const stepEl = target.closest("[data-pp-step-toggle]");
655
+ if (!stepEl) return;
656
+ event.preventDefault();
657
+ const id = Number(stepEl.dataset.ppStepToggle);
658
+ if (Number.isFinite(id) && id > 0) {
659
+ toggleGoalsExpanded(Math.trunc(id));
660
+ }
661
+ }
662
+ function startEvents() {
663
+ stopEvents?.();
664
+ stopEvents = opts.host.subscribeEvents({
665
+ onEvent: () => scheduleRefresh(90),
666
+ onError: () => {
667
+ }
668
+ });
669
+ }
670
+ el.addEventListener("click", handleClick);
671
+ el.addEventListener("keydown", handleKeydown);
672
+ if (typeof ResizeObserver !== "undefined" && opts.layout) {
673
+ reserveObserver = new ResizeObserver(() => scheduleReserveUpdate());
674
+ reserveObserver.observe(el);
675
+ }
676
+ render();
677
+ scheduleReserveUpdate();
678
+ startEvents();
679
+ void refreshAll();
680
+ return {
681
+ unmount() {
682
+ stopEvents?.();
683
+ stopEvents = null;
684
+ if (refreshTimer) {
685
+ window.clearTimeout(refreshTimer);
686
+ refreshTimer = null;
687
+ }
688
+ reserveObserver?.disconnect();
689
+ reserveObserver = null;
690
+ if (reserveRaf) {
691
+ window.cancelAnimationFrame(reserveRaf);
692
+ reserveRaf = 0;
693
+ }
694
+ el.removeEventListener("click", handleClick);
695
+ el.removeEventListener("keydown", handleKeydown);
696
+ el.innerHTML = "";
697
+ setReservePx(0);
698
+ }
699
+ };
700
+ }
701
+ export {
702
+ mount
703
+ };
704
+ //# sourceMappingURL=planpilot-todo-bar.js.map