@kata-sh/cli 0.1.0 → 0.1.2
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.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared next-action prompt for Kata extensions.
|
|
3
|
+
*
|
|
4
|
+
* Renders a consistent "step complete" UI at the end of every Kata stage:
|
|
5
|
+
*
|
|
6
|
+
* ─────────────────────────────────────────
|
|
7
|
+
* ✓ Phase 1 research complete
|
|
8
|
+
*
|
|
9
|
+
* [caller summary lines]
|
|
10
|
+
*
|
|
11
|
+
* [optional extra content block]
|
|
12
|
+
*
|
|
13
|
+
* Files written:
|
|
14
|
+
* .kata/phases/01-foo/01-RESEARCH.md
|
|
15
|
+
*
|
|
16
|
+
* › 1. Plan phase 1 ← recommended, pre-selected
|
|
17
|
+
* Create PLAN.md files for execution
|
|
18
|
+
*
|
|
19
|
+
* 2. Not yet
|
|
20
|
+
* Run /kata-plan-phase 1 when ready.
|
|
21
|
+
* ─────────────────────────────────────────
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
*
|
|
25
|
+
* const choice = await showNextAction(ctx, {
|
|
26
|
+
* title: "Phase 1 research complete",
|
|
27
|
+
* summary: ["6 libraries evaluated", "Stack: Phaser 3 + TypeScript"],
|
|
28
|
+
* files: ["/abs/path/to/01-RESEARCH.md"],
|
|
29
|
+
* extra: ["Wave 1: 01-01, 01-02 (parallel)", "Wave 2: 01-03"],
|
|
30
|
+
* actions: [
|
|
31
|
+
* { id: "plan", label: "Plan phase 1", description: "Create PLAN.md files for execution", recommended: true },
|
|
32
|
+
* { id: "later", label: "Discuss first", description: "Capture constraints before planning" },
|
|
33
|
+
* ],
|
|
34
|
+
* notYetMessage: "Run /kata-plan-phase 1 when ready.",
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // choice is one of the action ids, or "not_yet"
|
|
38
|
+
* if (choice === "plan") { ... }
|
|
39
|
+
*
|
|
40
|
+
* "Not yet" is always appended automatically as the last option.
|
|
41
|
+
* Pressing Escape also resolves as "not_yet".
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
45
|
+
import { type Theme } from "@mariozechner/pi-coding-agent";
|
|
46
|
+
import { Key, matchesKey, type TUI } from "@mariozechner/pi-tui";
|
|
47
|
+
import { makeUI } from "./ui.js";
|
|
48
|
+
|
|
49
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface NextAction {
|
|
52
|
+
/** Unique id returned when this action is chosen. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Short label shown in the list (e.g. "Plan phase 1"). */
|
|
55
|
+
label: string;
|
|
56
|
+
/** One-line description shown below the label. */
|
|
57
|
+
description: string;
|
|
58
|
+
/** Pre-selects this item and renders it with a (recommended) marker. At most one. */
|
|
59
|
+
recommended?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface NextActionOptions {
|
|
63
|
+
/** Bold heading after the ✓ — e.g. "Phase 1 research complete". */
|
|
64
|
+
title: string;
|
|
65
|
+
/**
|
|
66
|
+
* Stage-specific narrative lines rendered below the title.
|
|
67
|
+
* Keep these short and informative.
|
|
68
|
+
*/
|
|
69
|
+
summary?: string[];
|
|
70
|
+
/**
|
|
71
|
+
* Absolute paths to files that were written this step.
|
|
72
|
+
* Displayed as relative paths from cwd when possible.
|
|
73
|
+
*/
|
|
74
|
+
files?: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Optional extra content rendered between the file list and the actions.
|
|
77
|
+
* Each string is one display line — already formatted by the caller.
|
|
78
|
+
*/
|
|
79
|
+
extra?: string[];
|
|
80
|
+
/** The action choices. "Not yet" is always appended automatically. */
|
|
81
|
+
actions: NextAction[];
|
|
82
|
+
/**
|
|
83
|
+
* Message shown in the "Not yet" description line.
|
|
84
|
+
* e.g. "Run /kata-plan-phase 1 when ready."
|
|
85
|
+
*/
|
|
86
|
+
notYetMessage?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Current working directory — used to make file paths relative.
|
|
89
|
+
* Defaults to process.cwd().
|
|
90
|
+
*/
|
|
91
|
+
cwd?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Show the next-action prompt and return the chosen action id, or "not_yet".
|
|
96
|
+
*/
|
|
97
|
+
export async function showNextAction(
|
|
98
|
+
ctx: ExtensionCommandContext,
|
|
99
|
+
opts: NextActionOptions,
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
102
|
+
const notYetMessage = opts.notYetMessage ?? "Continue when ready.";
|
|
103
|
+
|
|
104
|
+
const allActions: NextAction[] = [
|
|
105
|
+
...opts.actions,
|
|
106
|
+
{ id: "not_yet", label: "Not yet", description: notYetMessage },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
const recommendedIdx = allActions.findIndex((a) => a.recommended);
|
|
110
|
+
const defaultIdx = recommendedIdx >= 0 ? recommendedIdx : 0;
|
|
111
|
+
|
|
112
|
+
const relativeFiles = (opts.files ?? []).map((f) => {
|
|
113
|
+
try {
|
|
114
|
+
const rel = f.startsWith(cwd)
|
|
115
|
+
? f.slice(cwd.length).replace(/^\//, "")
|
|
116
|
+
: f;
|
|
117
|
+
return rel || f;
|
|
118
|
+
} catch {
|
|
119
|
+
return f;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return ctx.ui.custom<string>((_tui: TUI, theme: Theme, _kb, done) => {
|
|
124
|
+
let cursorIdx = defaultIdx;
|
|
125
|
+
let cachedLines: string[] | undefined;
|
|
126
|
+
|
|
127
|
+
function refresh() {
|
|
128
|
+
cachedLines = undefined;
|
|
129
|
+
_tui.requestRender();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleInput(data: string) {
|
|
133
|
+
if (matchesKey(data, Key.up)) {
|
|
134
|
+
cursorIdx = Math.max(0, cursorIdx - 1);
|
|
135
|
+
refresh();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (matchesKey(data, Key.down)) {
|
|
139
|
+
cursorIdx = Math.min(allActions.length - 1, cursorIdx + 1);
|
|
140
|
+
refresh();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const num = parseInt(data, 10);
|
|
144
|
+
if (!isNaN(num) && num >= 1 && num <= allActions.length) {
|
|
145
|
+
done(allActions[num - 1].id);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
|
|
149
|
+
done(allActions[cursorIdx].id);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (matchesKey(data, Key.escape)) {
|
|
153
|
+
done("not_yet");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function render(width: number): string[] {
|
|
159
|
+
if (cachedLines) return cachedLines;
|
|
160
|
+
const ui = makeUI(theme, width);
|
|
161
|
+
const lines: string[] = [];
|
|
162
|
+
const push = (...rows: string[][]) => {
|
|
163
|
+
for (const r of rows) lines.push(...r);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ── Header — uses success colour to signal completion ────────────
|
|
167
|
+
// Note: next-action intentionally uses "success" for its bar/title
|
|
168
|
+
// to distinguish it from regular accent-coloured screens.
|
|
169
|
+
push(ui.bar());
|
|
170
|
+
push(ui.blank());
|
|
171
|
+
push(ui.header(` ✓ ${opts.title}`));
|
|
172
|
+
|
|
173
|
+
// ── Summary ──────────────────────────────────────────────────────
|
|
174
|
+
if (opts.summary && opts.summary.length > 0) {
|
|
175
|
+
push(ui.blank());
|
|
176
|
+
for (const line of opts.summary) push(ui.subtitle(` ${line}`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Files written ─────────────────────────────────────────────────
|
|
180
|
+
if (relativeFiles.length > 0) {
|
|
181
|
+
push(ui.blank());
|
|
182
|
+
push(ui.meta(" Files written:"));
|
|
183
|
+
for (const f of relativeFiles) push(ui.meta(` ${f}`));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Extra content ─────────────────────────────────────────────────
|
|
187
|
+
if (opts.extra && opts.extra.length > 0) {
|
|
188
|
+
push(ui.blank());
|
|
189
|
+
for (const line of opts.extra) push(ui.subtitle(` ${line}`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Actions ───────────────────────────────────────────────────────
|
|
193
|
+
push(ui.blank());
|
|
194
|
+
for (let i = 0; i < allActions.length; i++) {
|
|
195
|
+
const action = allActions[i];
|
|
196
|
+
const isSelected = i === cursorIdx;
|
|
197
|
+
const isNotYet = action.id === "not_yet";
|
|
198
|
+
const tag = action.recommended ? "(recommended)" : undefined;
|
|
199
|
+
|
|
200
|
+
if (isSelected) {
|
|
201
|
+
push(ui.actionSelected(i + 1, action.label, action.description, tag));
|
|
202
|
+
} else if (isNotYet) {
|
|
203
|
+
push(ui.actionDim(i + 1, action.label, action.description));
|
|
204
|
+
} else {
|
|
205
|
+
push(
|
|
206
|
+
ui.actionUnselected(i + 1, action.label, action.description, tag),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
push(ui.blank());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Footer ────────────────────────────────────────────────────────
|
|
213
|
+
const numHint = allActions.map((_, i) => `${i + 1}`).join("/");
|
|
214
|
+
push(
|
|
215
|
+
ui.hints([
|
|
216
|
+
`↑/↓ to choose`,
|
|
217
|
+
`${numHint} to quick-select`,
|
|
218
|
+
`enter to confirm`,
|
|
219
|
+
]),
|
|
220
|
+
);
|
|
221
|
+
push(ui.bar());
|
|
222
|
+
|
|
223
|
+
cachedLines = lines;
|
|
224
|
+
return lines;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
render,
|
|
229
|
+
invalidate: () => {
|
|
230
|
+
cachedLines = undefined;
|
|
231
|
+
},
|
|
232
|
+
handleInput,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared persistent progress/status panel widget.
|
|
3
|
+
*
|
|
4
|
+
* Renders an ordered list of progress items with status glyphs, optional
|
|
5
|
+
* badge, subtitle, metadata, and footer hints. Supports pulse animation
|
|
6
|
+
* for active items during agent execution.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* import { createProgressPanel } from "./shared/progress-widget.js";
|
|
11
|
+
*
|
|
12
|
+
* const panel = createProgressPanel(ctx.ui, {
|
|
13
|
+
* widgetKey: "workflow",
|
|
14
|
+
* statusKey: "workflow",
|
|
15
|
+
* statusPrefix: "wf",
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* panel.update(model); // render/re-render with new model
|
|
19
|
+
* panel.startPulse(); // animate active items
|
|
20
|
+
* panel.stopPulse(); // stop animation
|
|
21
|
+
* panel.dispose(); // remove widget and status
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ExtensionUIContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import type { TUI } from "@mariozechner/pi-tui";
|
|
26
|
+
import { makeUI, type ProgressStatus } from "./ui.js";
|
|
27
|
+
|
|
28
|
+
// ─── Exported types ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export type ProgressItemStatus = ProgressStatus;
|
|
31
|
+
|
|
32
|
+
export interface ProgressItem {
|
|
33
|
+
/** Display label */
|
|
34
|
+
label: string;
|
|
35
|
+
/** Drives glyph and color */
|
|
36
|
+
status: ProgressItemStatus;
|
|
37
|
+
/** Optional text after label — e.g. artifact type, task ID */
|
|
38
|
+
detail?: string;
|
|
39
|
+
/** Optional secondary line below item — e.g. "waiting for /workflow-continue" */
|
|
40
|
+
annotation?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ProgressPanelModel {
|
|
44
|
+
/** Panel title */
|
|
45
|
+
title: string;
|
|
46
|
+
/** Optional badge next to title — e.g. "RUNNING", "PAUSED" */
|
|
47
|
+
badge?: string;
|
|
48
|
+
/** Badge color control — maps to ProgressItemStatus color */
|
|
49
|
+
badgeStatus?: ProgressItemStatus;
|
|
50
|
+
/** Optional subtitle lines below title */
|
|
51
|
+
subtitle?: string[];
|
|
52
|
+
/** Ordered progress items */
|
|
53
|
+
items: ProgressItem[];
|
|
54
|
+
/** Optional metadata lines below items */
|
|
55
|
+
meta?: string[];
|
|
56
|
+
/** Optional footer hint strings */
|
|
57
|
+
hints?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ProgressPanelOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Widget key used with ctx.ui.setWidget(...).
|
|
63
|
+
* Must be unique per extension.
|
|
64
|
+
*/
|
|
65
|
+
widgetKey: string;
|
|
66
|
+
/**
|
|
67
|
+
* Status key used with ctx.ui.setStatus(...).
|
|
68
|
+
* Must be unique per extension.
|
|
69
|
+
*/
|
|
70
|
+
statusKey: string;
|
|
71
|
+
/**
|
|
72
|
+
* Short prefix for footer status text.
|
|
73
|
+
* Example: "wf" produces "wf:2/3 RUNNING"
|
|
74
|
+
*/
|
|
75
|
+
statusPrefix: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ProgressPanel {
|
|
79
|
+
/** Update the widget with a new model. Triggers re-render. */
|
|
80
|
+
update(model: ProgressPanelModel): void;
|
|
81
|
+
/** Start pulsing items with status "active". */
|
|
82
|
+
startPulse(): void;
|
|
83
|
+
/** Stop pulsing. Active items render at full brightness. */
|
|
84
|
+
stopPulse(): void;
|
|
85
|
+
/** Remove the widget and status from the UI. */
|
|
86
|
+
dispose(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Internal constants ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const PULSE_INTERVAL_MS = 500;
|
|
92
|
+
|
|
93
|
+
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create and register a persistent progress widget.
|
|
97
|
+
*
|
|
98
|
+
* @param ui The `ctx.ui` object from ExtensionContext or ExtensionCommandContext
|
|
99
|
+
* @param options Widget key, status key, and status prefix
|
|
100
|
+
* @returns ProgressPanel controller
|
|
101
|
+
*/
|
|
102
|
+
export function createProgressPanel(
|
|
103
|
+
ui: ExtensionUIContext,
|
|
104
|
+
options: ProgressPanelOptions,
|
|
105
|
+
): ProgressPanel {
|
|
106
|
+
const { widgetKey, statusKey, statusPrefix } = options;
|
|
107
|
+
|
|
108
|
+
// ── Internal state ────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
let currentModel: ProgressPanelModel | null = null;
|
|
111
|
+
let stateVersion = 0;
|
|
112
|
+
let cachedLines: string[] | undefined;
|
|
113
|
+
let cachedWidth: number | undefined;
|
|
114
|
+
let cachedVersion = -1;
|
|
115
|
+
let pulseBright = true;
|
|
116
|
+
let pulseTimer: ReturnType<typeof setInterval> | null = null;
|
|
117
|
+
let widgetRef: { invalidate: () => void; requestRender: () => void } | null = null;
|
|
118
|
+
|
|
119
|
+
// ── Footer status ─────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function updateFooterStatus(): void {
|
|
122
|
+
if (!currentModel) return;
|
|
123
|
+
const { items, badge } = currentModel;
|
|
124
|
+
const total = items.length;
|
|
125
|
+
let current = 0;
|
|
126
|
+
|
|
127
|
+
// Find first active item index (1-based)
|
|
128
|
+
const activeIdx = items.findIndex((it) => it.status === "active");
|
|
129
|
+
if (activeIdx >= 0) {
|
|
130
|
+
current = activeIdx + 1;
|
|
131
|
+
} else {
|
|
132
|
+
// Count done items + 1
|
|
133
|
+
current = items.filter((it) => it.status === "done").length + 1;
|
|
134
|
+
}
|
|
135
|
+
if (current > total) current = total;
|
|
136
|
+
|
|
137
|
+
const badgePart = badge ? ` ${badge}` : "";
|
|
138
|
+
const statusText = ui.theme.fg("accent", `${statusPrefix}:${current}/${total}${badgePart}`);
|
|
139
|
+
ui.setStatus(statusKey, statusText);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Render function ───────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
function renderPanel(width: number, theme: Theme): string[] {
|
|
145
|
+
// Version-based cache check
|
|
146
|
+
if (cachedLines && cachedWidth === width && cachedVersion === stateVersion) {
|
|
147
|
+
return cachedLines;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!currentModel) return [];
|
|
151
|
+
|
|
152
|
+
const uiHelper = makeUI(theme, width);
|
|
153
|
+
const lines: string[] = [];
|
|
154
|
+
const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
|
|
155
|
+
const model = currentModel;
|
|
156
|
+
|
|
157
|
+
// 1. Top bar
|
|
158
|
+
push(uiHelper.bar());
|
|
159
|
+
|
|
160
|
+
// 2. Title area — title with optional inline badge
|
|
161
|
+
if (model.badge && model.badgeStatus) {
|
|
162
|
+
const titleText = uiHelper.header(model.title)[0];
|
|
163
|
+
const badgeGlyph = uiHelper.statusGlyph(model.badgeStatus);
|
|
164
|
+
const badgeLabel = uiHelper.statusBadge(model.badge, model.badgeStatus)[0];
|
|
165
|
+
lines.push(`${titleText} ${badgeGlyph} ${badgeLabel.trimStart()}`);
|
|
166
|
+
} else {
|
|
167
|
+
push(uiHelper.header(model.title));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Subtitle
|
|
171
|
+
if (model.subtitle?.length) {
|
|
172
|
+
for (const line of model.subtitle) {
|
|
173
|
+
push(uiHelper.meta(line));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 4. Blank line
|
|
178
|
+
push(uiHelper.blank());
|
|
179
|
+
|
|
180
|
+
// 5. Items
|
|
181
|
+
for (const item of model.items) {
|
|
182
|
+
// Pulse: when pulseBright is false and item is active, render as pending (dimmed)
|
|
183
|
+
const renderStatus: ProgressStatus = (!pulseBright && item.status === "active")
|
|
184
|
+
? "pending"
|
|
185
|
+
: item.status;
|
|
186
|
+
|
|
187
|
+
push(uiHelper.progressItem(item.label, renderStatus, {
|
|
188
|
+
detail: item.detail,
|
|
189
|
+
emphasized: item.status === "active",
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
if (item.annotation) {
|
|
193
|
+
push(uiHelper.progressAnnotation(item.annotation));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 6. Blank line (if meta or hints follow)
|
|
198
|
+
if (model.meta?.length || model.hints?.length) {
|
|
199
|
+
push(uiHelper.blank());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 7. Meta
|
|
203
|
+
if (model.meta?.length) {
|
|
204
|
+
for (const line of model.meta) {
|
|
205
|
+
push(uiHelper.meta(line));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 8. Hints
|
|
210
|
+
if (model.hints?.length) {
|
|
211
|
+
push(uiHelper.hints(model.hints));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 9. Bottom bar
|
|
215
|
+
push(uiHelper.bar());
|
|
216
|
+
|
|
217
|
+
cachedLines = lines;
|
|
218
|
+
cachedWidth = width;
|
|
219
|
+
cachedVersion = stateVersion;
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Register widget ───────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
ui.setWidget(widgetKey, (tui: TUI, theme: Theme) => {
|
|
226
|
+
widgetRef = {
|
|
227
|
+
invalidate: () => { cachedLines = undefined; },
|
|
228
|
+
requestRender: () => tui.requestRender(),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
render(width: number): string[] {
|
|
233
|
+
return renderPanel(width, theme);
|
|
234
|
+
},
|
|
235
|
+
invalidate() {
|
|
236
|
+
cachedLines = undefined;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ── Controller ────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
update(model: ProgressPanelModel): void {
|
|
245
|
+
currentModel = model;
|
|
246
|
+
stateVersion++;
|
|
247
|
+
cachedLines = undefined;
|
|
248
|
+
updateFooterStatus();
|
|
249
|
+
if (widgetRef) widgetRef.requestRender();
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
startPulse(): void {
|
|
253
|
+
if (pulseTimer) return; // already pulsing
|
|
254
|
+
pulseTimer = setInterval(() => {
|
|
255
|
+
pulseBright = !pulseBright;
|
|
256
|
+
cachedLines = undefined;
|
|
257
|
+
if (widgetRef) widgetRef.requestRender();
|
|
258
|
+
}, PULSE_INTERVAL_MS);
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
stopPulse(): void {
|
|
262
|
+
if (pulseTimer) {
|
|
263
|
+
clearInterval(pulseTimer);
|
|
264
|
+
pulseTimer = null;
|
|
265
|
+
}
|
|
266
|
+
pulseBright = true;
|
|
267
|
+
cachedLines = undefined;
|
|
268
|
+
if (widgetRef) widgetRef.requestRender();
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
dispose(): void {
|
|
272
|
+
if (pulseTimer) {
|
|
273
|
+
clearInterval(pulseTimer);
|
|
274
|
+
pulseTimer = null;
|
|
275
|
+
}
|
|
276
|
+
ui.setWidget(widgetKey, undefined);
|
|
277
|
+
ui.setStatus(statusKey, undefined);
|
|
278
|
+
currentModel = null;
|
|
279
|
+
widgetRef = null;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared thinking/spinner widget.
|
|
3
|
+
*
|
|
4
|
+
* Shows an animated spinner with a label and an optional live-preview of
|
|
5
|
+
* streamed text (e.g. LLM output) while a background operation is running.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* import { showThinkingWidget } from "./shared/thinking-widget.js";
|
|
10
|
+
*
|
|
11
|
+
* const widget = showThinkingWidget("Generating questions…", ctx);
|
|
12
|
+
*
|
|
13
|
+
* // Optionally stream partial text into the preview line:
|
|
14
|
+
* widget.setText(partialLlmOutput);
|
|
15
|
+
*
|
|
16
|
+
* // Always dispose when done — removes the widget from the UI:
|
|
17
|
+
* widget.dispose();
|
|
18
|
+
*
|
|
19
|
+
* Each call gets a unique widget key derived from a monotonic counter, so
|
|
20
|
+
* multiple widgets can safely coexist without key collisions.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import { type Theme } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
import { truncateToWidth, type TUI } from "@mariozechner/pi-tui";
|
|
26
|
+
|
|
27
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface ThinkingWidget {
|
|
30
|
+
/**
|
|
31
|
+
* Update the streamed-text preview line.
|
|
32
|
+
* Pass the full accumulated text — the widget trims and previews the tail.
|
|
33
|
+
*/
|
|
34
|
+
setText(text: string): void;
|
|
35
|
+
/** Remove the widget from the UI. Always call this when the operation completes. */
|
|
36
|
+
dispose(): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Internal constants ───────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
42
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
43
|
+
const PREVIEW_MAX_CHARS = 120;
|
|
44
|
+
|
|
45
|
+
let widgetCounter = 0;
|
|
46
|
+
|
|
47
|
+
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Show an animated thinking spinner as a TUI widget.
|
|
51
|
+
*
|
|
52
|
+
* @param label Short description of what is happening, e.g. "Writing PROJECT.md…"
|
|
53
|
+
* @param ctx Extension command context
|
|
54
|
+
* @returns Handle with setText() and dispose()
|
|
55
|
+
*/
|
|
56
|
+
export function showThinkingWidget(label: string, ctx: ExtensionCommandContext): ThinkingWidget {
|
|
57
|
+
const widgetKey = `thinking-widget-${++widgetCounter}`;
|
|
58
|
+
|
|
59
|
+
let streamedText = "";
|
|
60
|
+
let widgetRef: { invalidate: () => void; requestRender: () => void } | null = null;
|
|
61
|
+
|
|
62
|
+
ctx.ui.setWidget(widgetKey, (tui: TUI, theme: Theme) => {
|
|
63
|
+
let frame = 0;
|
|
64
|
+
let cachedLines: string[] | undefined;
|
|
65
|
+
|
|
66
|
+
const interval = setInterval(() => {
|
|
67
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
68
|
+
cachedLines = undefined;
|
|
69
|
+
tui.requestRender();
|
|
70
|
+
}, SPINNER_INTERVAL_MS);
|
|
71
|
+
|
|
72
|
+
widgetRef = {
|
|
73
|
+
invalidate: () => { cachedLines = undefined; },
|
|
74
|
+
requestRender: () => tui.requestRender(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
render(width: number): string[] {
|
|
79
|
+
if (cachedLines) return cachedLines;
|
|
80
|
+
const spinner = theme.fg("accent", SPINNER_FRAMES[frame]);
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
lines.push(truncateToWidth(` ${spinner} ${theme.fg("muted", label)}`, width));
|
|
83
|
+
if (streamedText) {
|
|
84
|
+
const preview = streamedText.replace(/\s+/g, " ").trim().slice(-PREVIEW_MAX_CHARS);
|
|
85
|
+
lines.push(truncateToWidth(` ${theme.fg("dim", preview)}`, width));
|
|
86
|
+
}
|
|
87
|
+
cachedLines = lines;
|
|
88
|
+
return lines;
|
|
89
|
+
},
|
|
90
|
+
invalidate() { cachedLines = undefined; },
|
|
91
|
+
dispose() { clearInterval(interval); },
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
setText(text: string) {
|
|
97
|
+
streamedText = text;
|
|
98
|
+
if (widgetRef) {
|
|
99
|
+
widgetRef.invalidate();
|
|
100
|
+
widgetRef.requestRender();
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
dispose() {
|
|
104
|
+
ctx.ui.setWidget(widgetKey, undefined);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|