@dieulc/workflow 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -28
- package/commit-policy.ts +43 -0
- package/index.ts +3017 -1119
- package/model-default.ts +535 -0
- package/package.json +8 -2
- package/picker.ts +246 -0
- package/profiles.ts +73 -0
- package/questionnaire-layout.ts +340 -0
- package/questionnaire-view.ts +909 -0
- package/review-runtime.ts +267 -76
- package/review-ui.ts +27 -4
- package/review.ts +516 -16
- package/tab-editor.ts +147 -0
- package/tab-policy.ts +42 -0
- package/utils.ts +444 -69
package/tab-editor.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow editor: gives Tab back to autocomplete while a suggestion list is
|
|
3
|
+
* visible, and lets the existing `tab` extension shortcut cycle modes when it
|
|
4
|
+
* is not.
|
|
5
|
+
*
|
|
6
|
+
* Why a custom editor is needed: Pi's `CustomEditor.handleInput()` dispatches
|
|
7
|
+
* extension shortcuts before any editor/autocomplete handling, and the
|
|
8
|
+
* workflow registers a raw `tab` shortcut. A plugin cannot make that dispatch
|
|
9
|
+
* conditional, so the workflow installs this thin subclass through the public
|
|
10
|
+
* `ctx.ui.setEditorComponent()` API. The subclass only changes the precedence
|
|
11
|
+
* for the completion branch; every other key and every other extension
|
|
12
|
+
* shortcut keeps the normal path.
|
|
13
|
+
*
|
|
14
|
+
* Public-API constraints:
|
|
15
|
+
* - `isShowingAutocomplete()` is public.
|
|
16
|
+
* - The editor's `autocompleteList`, `autocompletePrefix` and
|
|
17
|
+
* `handleTabCompletion()` are private, so the only clean way to accept the
|
|
18
|
+
* highlighted item is to suppress the extension bridge for one synchronous
|
|
19
|
+
* `super.handleInput()` call.
|
|
20
|
+
* - `onExtensionShortcut` is installed by the core *after* this class is
|
|
21
|
+
* constructed. Do not declare a same-named field: an own handler would stop
|
|
22
|
+
* the core from installing its bridge and silently break every extension
|
|
23
|
+
* shortcut.
|
|
24
|
+
*
|
|
25
|
+
* The history helpers keep prompt history alive across the editor swap that
|
|
26
|
+
* `setCustomEditorComponent()` performs: the core copies the editor text but not
|
|
27
|
+
* the `history` array.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
CustomEditor,
|
|
32
|
+
type KeybindingsManager,
|
|
33
|
+
} from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import type { Editor, EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
35
|
+
import { decideTabAction } from "./tab-policy.ts";
|
|
36
|
+
|
|
37
|
+
/** Minimal structural shape of a session entry that can carry user text. */
|
|
38
|
+
export interface PromptHistoryEntry {
|
|
39
|
+
type?: string;
|
|
40
|
+
message?: {
|
|
41
|
+
role?: string;
|
|
42
|
+
content?:
|
|
43
|
+
| string
|
|
44
|
+
| ReadonlyArray<{ type?: string; text?: string }>;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* User prompt texts from session entries, oldest first.
|
|
50
|
+
*
|
|
51
|
+
* Mirrors pi's `getUserMessageText()`: string content or the concatenation of
|
|
52
|
+
* text blocks. Entries without user text (assistant/tool/system, or an empty
|
|
53
|
+
* message) are skipped.
|
|
54
|
+
*/
|
|
55
|
+
export function promptHistoryTexts(
|
|
56
|
+
entries: readonly PromptHistoryEntry[],
|
|
57
|
+
): string[] {
|
|
58
|
+
const texts: string[] = [];
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (!entry || entry.type !== "message") continue;
|
|
61
|
+
const message = entry.message;
|
|
62
|
+
if (!message || message.role !== "user") continue;
|
|
63
|
+
|
|
64
|
+
const content = message.content;
|
|
65
|
+
let text = "";
|
|
66
|
+
if (typeof content === "string") {
|
|
67
|
+
text = content;
|
|
68
|
+
} else {
|
|
69
|
+
for (const block of content ?? []) {
|
|
70
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
71
|
+
text += block.text;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (text.trim()) texts.push(text);
|
|
76
|
+
}
|
|
77
|
+
return texts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Seed a fresh editor's prompt history from session entries.
|
|
82
|
+
*
|
|
83
|
+
* `Editor.addToHistory()` unshifts, so feeding oldest→newest leaves the newest
|
|
84
|
+
* prompt at the cursor end, exactly like the host's own population path.
|
|
85
|
+
*/
|
|
86
|
+
export function seedPromptHistory(
|
|
87
|
+
editor: Pick<Editor, "addToHistory">,
|
|
88
|
+
entries: readonly PromptHistoryEntry[],
|
|
89
|
+
): void {
|
|
90
|
+
for (const text of promptHistoryTexts(entries)) {
|
|
91
|
+
editor.addToHistory(text);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class WorkflowTabEditor extends CustomEditor {
|
|
96
|
+
private readonly tabKeybindings: KeybindingsManager;
|
|
97
|
+
private hostHistoryAdded = false;
|
|
98
|
+
|
|
99
|
+
constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
100
|
+
super(tui, theme, keybindings);
|
|
101
|
+
this.tabKeybindings = keybindings;
|
|
102
|
+
// The default 1.4.0 editor renders the working spinner in its top border.
|
|
103
|
+
// `embedWorkingStatus` is absent from the 0.84.4 constructor options type
|
|
104
|
+
// this package compiles against, but the runtime check is field-based, so
|
|
105
|
+
// assigning it after `super` restores the default behaviour and is inert on
|
|
106
|
+
// older versions.
|
|
107
|
+
(this as any).embedWorkingStatus = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True once the host has populated this editor's prompt history. */
|
|
111
|
+
hasHostHistory(): boolean {
|
|
112
|
+
return this.hostHistoryAdded;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override addToHistory(text: string): void {
|
|
116
|
+
if (text.trim()) this.hostHistoryAdded = true;
|
|
117
|
+
super.addToHistory(text);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
override handleInput(data: string): void {
|
|
121
|
+
const action = decideTabAction({
|
|
122
|
+
data,
|
|
123
|
+
keybindings: this.tabKeybindings,
|
|
124
|
+
editor: this,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (action === "complete") {
|
|
128
|
+
// Suppress only the extension-shortcut bridge for this synchronous call.
|
|
129
|
+
// `super.handleInput()` then reaches the editor's Tab branch, which
|
|
130
|
+
// applies the highlighted item. `finally` restores the bridge so the
|
|
131
|
+
// workflow `tab` shortcut and every other extension shortcut keep
|
|
132
|
+
// working for the next key.
|
|
133
|
+
const forwarded = this.onExtensionShortcut;
|
|
134
|
+
this.onExtensionShortcut = undefined;
|
|
135
|
+
try {
|
|
136
|
+
super.handleInput(data);
|
|
137
|
+
} finally {
|
|
138
|
+
this.onExtensionShortcut = forwarded;
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// "cycle" (no visible list) and "ignore" (not Tab / disabled binding) use
|
|
144
|
+
// the normal CustomEditor path: extension shortcuts first, then the editor.
|
|
145
|
+
super.handleInput(data);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/tab-policy.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab precedence between Pi's autocomplete and the workflow mode cycle.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `CustomEditor.handleInput()` checks extension shortcuts *before* the
|
|
5
|
+
* editor, so the workflow's registered `tab` shortcut would otherwise swallow
|
|
6
|
+
* every Tab — including the one that accepts the highlighted command, file or
|
|
7
|
+
* document suggestion. The workflow editor asks this policy first:
|
|
8
|
+
*
|
|
9
|
+
* suggestion list visible -> complete (apply the highlighted item)
|
|
10
|
+
* no suggestion list -> cycle (Plan → Build → Default)
|
|
11
|
+
* another key -> ignore (let CustomEditor/Editor handle it)
|
|
12
|
+
* Tab binding disabled -> ignore (never turn a disabled Tab into accept)
|
|
13
|
+
*
|
|
14
|
+
* The rule is deliberately based on menu visibility only, matching Pi Desktop's
|
|
15
|
+
* `Composer.tsx` `menuKey` logic. A `/command` line whose list has not rendered
|
|
16
|
+
* yet is allowed to cycle; the list wins only while it is actually visible.
|
|
17
|
+
*
|
|
18
|
+
* Pure module (type-only imports) so the precedence is unit-testable without a
|
|
19
|
+
* TUI.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Keybinding } from "@earendil-works/pi-tui";
|
|
23
|
+
|
|
24
|
+
export type TabAction = "ignore" | "complete" | "cycle";
|
|
25
|
+
|
|
26
|
+
export interface TabEditorView {
|
|
27
|
+
/** True while a non-empty autocomplete list is visible. */
|
|
28
|
+
isShowingAutocomplete(): boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TabKeyMatcher {
|
|
32
|
+
matches(data: string, keybinding: Keybinding): boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function decideTabAction(args: {
|
|
36
|
+
data: string;
|
|
37
|
+
keybindings: TabKeyMatcher;
|
|
38
|
+
editor: TabEditorView;
|
|
39
|
+
}): TabAction {
|
|
40
|
+
if (!args.keybindings.matches(args.data, "tui.input.tab")) return "ignore";
|
|
41
|
+
return args.editor.isShowingAutocomplete() ? "complete" : "cycle";
|
|
42
|
+
}
|