@dungle-scrubs/tallow 0.8.21 → 0.8.23
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/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +2 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +82 -0
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +68 -1
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-relay.d.ts +40 -7
- package/dist/workspace-transition-relay.d.ts.map +1 -1
- package/dist/workspace-transition-relay.js +81 -16
- package/dist/workspace-transition-relay.js.map +1 -1
- package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
- package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
- package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
- package/extensions/_shared/atomic-write.ts +1 -1
- package/extensions/_shared/bordered-box.ts +102 -0
- package/extensions/_shared/interop-events.ts +5 -0
- package/extensions/_shared/pid-registry.ts +1 -1
- package/extensions/agent-commands-tool/index.ts +4 -1
- package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
- package/extensions/background-task-tool/index.ts +139 -221
- package/extensions/bash-tool-enhanced/index.ts +1 -75
- package/extensions/cd-tool/index.ts +2 -2
- package/extensions/context-fork/spawn.ts +4 -1
- package/extensions/health/index.ts +6 -6
- package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
- package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
- package/extensions/hooks/index.ts +27 -4
- package/extensions/loop/__tests__/loop.test.ts +168 -4
- package/extensions/loop/extension.json +6 -5
- package/extensions/loop/index.ts +242 -31
- package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
- package/extensions/plan-mode-tool/index.ts +103 -41
- package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
- package/extensions/prompt-suggestions/index.ts +41 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
- package/extensions/slash-command-bridge/extension.json +1 -1
- package/extensions/slash-command-bridge/index.ts +230 -116
- package/extensions/subagent-tool/index.ts +2 -2
- package/extensions/subagent-tool/process.ts +4 -5
- package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
- package/extensions/teams-tool/dashboard.ts +3 -5
- package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
- package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
- package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
- package/extensions/wezterm-pane-control/index.ts +113 -8
- package/package.json +6 -4
- package/packages/tallow-tui/README.md +51 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
- package/packages/tallow-tui/dist/autocomplete.js +564 -0
- package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
- package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
- package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
- package/packages/tallow-tui/dist/border-styles.js +46 -0
- package/packages/tallow-tui/dist/border-styles.js.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
- package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
- package/packages/tallow-tui/dist/components/box.d.ts +22 -0
- package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/box.js +104 -0
- package/packages/tallow-tui/dist/components/box.js.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
- package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/editor.js +1766 -0
- package/packages/tallow-tui/dist/components/editor.js.map +1 -0
- package/packages/tallow-tui/dist/components/image.d.ts +126 -0
- package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/image.js +245 -0
- package/packages/tallow-tui/dist/components/image.js.map +1 -0
- package/packages/tallow-tui/dist/components/input.d.ts +37 -0
- package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/input.js +439 -0
- package/packages/tallow-tui/dist/components/input.js.map +1 -0
- package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
- package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/loader.js +146 -0
- package/packages/tallow-tui/dist/components/loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.js +633 -0
- package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.js +156 -0
- package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.js +189 -0
- package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.js +23 -0
- package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
- package/packages/tallow-tui/dist/components/text.d.ts +19 -0
- package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/text.js +91 -0
- package/packages/tallow-tui/dist/components/text.js.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
- package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
- package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
- package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
- package/packages/tallow-tui/dist/editor-component.js +2 -0
- package/packages/tallow-tui/dist/editor-component.js.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.js +107 -0
- package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
- package/packages/tallow-tui/dist/index.d.ts +25 -0
- package/packages/tallow-tui/dist/index.d.ts.map +1 -0
- package/packages/tallow-tui/dist/index.js +35 -0
- package/packages/tallow-tui/dist/index.js.map +1 -0
- package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
- package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keybindings.js +114 -0
- package/packages/tallow-tui/dist/keybindings.js.map +1 -0
- package/packages/tallow-tui/dist/keys.d.ts +168 -0
- package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keys.js +971 -0
- package/packages/tallow-tui/dist/keys.js.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.js +44 -0
- package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
- package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.js +460 -0
- package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
- package/packages/tallow-tui/dist/terminal.d.ts +102 -0
- package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal.js +263 -0
- package/packages/tallow-tui/dist/terminal.js.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
- package/packages/tallow-tui/dist/tui.d.ts +239 -0
- package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
- package/packages/tallow-tui/dist/tui.js +1058 -0
- package/packages/tallow-tui/dist/tui.js.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.js +25 -0
- package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
- package/packages/tallow-tui/dist/utils.d.ts +96 -0
- package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
- package/packages/tallow-tui/dist/utils.js +843 -0
- package/packages/tallow-tui/dist/utils.js.map +1 -0
- package/packages/tallow-tui/package.json +24 -0
- package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
- package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
- package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
- package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
- package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
- package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
- package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
- package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
- package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
- package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
- package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
- package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
- package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
- package/packages/tallow-tui/src/autocomplete.ts +716 -0
- package/packages/tallow-tui/src/border-styles.ts +60 -0
- package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
- package/packages/tallow-tui/src/components/box.ts +137 -0
- package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
- package/packages/tallow-tui/src/components/editor.ts +2143 -0
- package/packages/tallow-tui/src/components/image.ts +315 -0
- package/packages/tallow-tui/src/components/input.ts +522 -0
- package/packages/tallow-tui/src/components/loader.ts +187 -0
- package/packages/tallow-tui/src/components/markdown.ts +780 -0
- package/packages/tallow-tui/src/components/select-list.ts +197 -0
- package/packages/tallow-tui/src/components/settings-list.ts +264 -0
- package/packages/tallow-tui/src/components/spacer.ts +28 -0
- package/packages/tallow-tui/src/components/text.ts +113 -0
- package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
- package/packages/tallow-tui/src/editor-component.ts +92 -0
- package/packages/tallow-tui/src/fuzzy.ts +133 -0
- package/packages/tallow-tui/src/index.ts +118 -0
- package/packages/tallow-tui/src/keybindings.ts +183 -0
- package/packages/tallow-tui/src/keys.ts +1189 -0
- package/packages/tallow-tui/src/kill-ring.ts +46 -0
- package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
- package/packages/tallow-tui/src/terminal-image.ts +619 -0
- package/packages/tallow-tui/src/terminal.ts +350 -0
- package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
- package/packages/tallow-tui/src/tui.ts +1336 -0
- package/packages/tallow-tui/src/undo-stack.ts +28 -0
- package/packages/tallow-tui/src/utils.ts +948 -0
- package/packages/tallow-tui/tsconfig.build.json +21 -0
- package/runtime/agent-runner.ts +20 -0
- package/runtime/atomic-write.ts +8 -0
- package/runtime/otel.ts +12 -0
- package/runtime/resolve-module.ts +23 -0
- package/runtime/runtime-path-provider.ts +12 -0
- package/runtime/runtime-provenance.ts +17 -0
- package/runtime/workspace-transition-relay.ts +21 -0
- package/runtime/workspace-transition.ts +29 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
|
|
5
|
+
import planModeExtension from "../index.js";
|
|
6
|
+
import type { TodoItem } from "../utils.js";
|
|
7
|
+
|
|
8
|
+
const BASELINE_TOOLS = [
|
|
9
|
+
"read",
|
|
10
|
+
"bash",
|
|
11
|
+
"grep",
|
|
12
|
+
"find",
|
|
13
|
+
"ls",
|
|
14
|
+
"edit",
|
|
15
|
+
"write",
|
|
16
|
+
"subagent",
|
|
17
|
+
"bg_bash",
|
|
18
|
+
"questionnaire",
|
|
19
|
+
"plan_mode",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register mock tools for the test session.
|
|
24
|
+
*
|
|
25
|
+
* @param pi - Extension API
|
|
26
|
+
*/
|
|
27
|
+
function registerMockTools(pi: ExtensionAPI): void {
|
|
28
|
+
for (const name of [
|
|
29
|
+
"read",
|
|
30
|
+
"bash",
|
|
31
|
+
"grep",
|
|
32
|
+
"find",
|
|
33
|
+
"ls",
|
|
34
|
+
"edit",
|
|
35
|
+
"write",
|
|
36
|
+
"subagent",
|
|
37
|
+
"bg_bash",
|
|
38
|
+
"questionnaire",
|
|
39
|
+
] as const) {
|
|
40
|
+
pi.registerTool({
|
|
41
|
+
name,
|
|
42
|
+
label: name,
|
|
43
|
+
description: `Mock ${name}`,
|
|
44
|
+
parameters: Type.Object({}),
|
|
45
|
+
async execute() {
|
|
46
|
+
return { content: [{ type: "text", text: `${name}-ok` }], details: {} };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build persisted session entries that place the extension in execution mode.
|
|
54
|
+
*
|
|
55
|
+
* @param todos - Todo items for the plan
|
|
56
|
+
* @returns Array of session entries
|
|
57
|
+
*/
|
|
58
|
+
function executionModeEntries(todos: TodoItem[]): unknown[] {
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
type: "custom",
|
|
62
|
+
customType: "plan-mode",
|
|
63
|
+
data: {
|
|
64
|
+
enabled: false,
|
|
65
|
+
executing: true,
|
|
66
|
+
normalTools: [...BASELINE_TOOLS],
|
|
67
|
+
todos,
|
|
68
|
+
currentStepIndex: 0,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{ type: "custom", customType: "plan-mode-execute" },
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create an extension context with a configurable `select` stub.
|
|
77
|
+
*
|
|
78
|
+
* @param entries - Session entries for state restoration
|
|
79
|
+
* @param selectReturn - Value that ctx.ui.select() resolves to
|
|
80
|
+
* @returns Context and a record of select calls
|
|
81
|
+
*/
|
|
82
|
+
function createUIContext(
|
|
83
|
+
entries: unknown[] = [],
|
|
84
|
+
selectReturn?: string
|
|
85
|
+
): { ctx: ExtensionContext; selectCalls: Array<{ title: string; options: string[] }> } {
|
|
86
|
+
const selectCalls: Array<{ title: string; options: string[] }> = [];
|
|
87
|
+
const ctx = {
|
|
88
|
+
cwd: process.cwd(),
|
|
89
|
+
hasUI: true,
|
|
90
|
+
ui: {
|
|
91
|
+
notify() {},
|
|
92
|
+
setStatus() {},
|
|
93
|
+
setEditorComponent() {},
|
|
94
|
+
setWidget() {},
|
|
95
|
+
setWorkingMessage() {},
|
|
96
|
+
async select(title: string, options: string[]) {
|
|
97
|
+
selectCalls.push({ title, options });
|
|
98
|
+
return selectReturn;
|
|
99
|
+
},
|
|
100
|
+
async editor() {
|
|
101
|
+
return undefined;
|
|
102
|
+
},
|
|
103
|
+
theme: {
|
|
104
|
+
fg(_token: string, value: string) {
|
|
105
|
+
return value;
|
|
106
|
+
},
|
|
107
|
+
bg(_token: string, value: string) {
|
|
108
|
+
return value;
|
|
109
|
+
},
|
|
110
|
+
strikethrough(value: string) {
|
|
111
|
+
return value;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
} as never,
|
|
115
|
+
sessionManager: {
|
|
116
|
+
getEntries() {
|
|
117
|
+
return entries;
|
|
118
|
+
},
|
|
119
|
+
} as never,
|
|
120
|
+
} as unknown as ExtensionContext;
|
|
121
|
+
return { ctx, selectCalls };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a headless context (no UI).
|
|
126
|
+
*
|
|
127
|
+
* @param entries - Session entries for state restoration
|
|
128
|
+
* @returns Extension context with hasUI=false
|
|
129
|
+
*/
|
|
130
|
+
function createHeadlessContext(entries: unknown[] = []): ExtensionContext {
|
|
131
|
+
return {
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
hasUI: false,
|
|
134
|
+
ui: {
|
|
135
|
+
notify() {},
|
|
136
|
+
setStatus() {},
|
|
137
|
+
setEditorComponent() {},
|
|
138
|
+
setWidget() {},
|
|
139
|
+
setWorkingMessage() {},
|
|
140
|
+
theme: {
|
|
141
|
+
fg(_token: string, value: string) {
|
|
142
|
+
return value;
|
|
143
|
+
},
|
|
144
|
+
strikethrough(value: string) {
|
|
145
|
+
return value;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
} as never,
|
|
149
|
+
sessionManager: {
|
|
150
|
+
getEntries() {
|
|
151
|
+
return entries;
|
|
152
|
+
},
|
|
153
|
+
} as never,
|
|
154
|
+
} as unknown as ExtensionContext;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const SAMPLE_TODOS: TodoItem[] = [
|
|
158
|
+
{ step: 1, text: "Add error handling", completed: false },
|
|
159
|
+
{ step: 2, text: "Write tests", completed: false },
|
|
160
|
+
{ step: 3, text: "Update docs", completed: false },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
describe("agent_end execution mode — partial completion", () => {
|
|
164
|
+
let harness: ExtensionHarness;
|
|
165
|
+
|
|
166
|
+
beforeEach(async () => {
|
|
167
|
+
harness = ExtensionHarness.create();
|
|
168
|
+
await harness.loadExtension(registerMockTools);
|
|
169
|
+
await harness.loadExtension(planModeExtension);
|
|
170
|
+
harness.api.setActiveTools([...BASELINE_TOOLS]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("shows select menu when agent finishes with incomplete steps", async () => {
|
|
174
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
175
|
+
const entries = executionModeEntries(todos);
|
|
176
|
+
const { ctx, selectCalls } = createUIContext(entries, "Abort plan");
|
|
177
|
+
|
|
178
|
+
// Restore execution mode state
|
|
179
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
180
|
+
|
|
181
|
+
// Agent finishes a turn — no [DONE:n] markers
|
|
182
|
+
await harness.fireEvent(
|
|
183
|
+
"agent_end",
|
|
184
|
+
{ messages: [{ role: "assistant", content: [{ type: "text", text: "Did some work." }] }] },
|
|
185
|
+
ctx
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(selectCalls).toHaveLength(1);
|
|
189
|
+
expect(selectCalls[0].title).toContain("0/3");
|
|
190
|
+
expect(selectCalls[0].options).toEqual([
|
|
191
|
+
"Continue execution",
|
|
192
|
+
"Provide guidance",
|
|
193
|
+
"Mark plan as done",
|
|
194
|
+
"Abort plan",
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("shows correct count when some steps are completed", async () => {
|
|
199
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
200
|
+
todos[0].completed = true; // step 1 done
|
|
201
|
+
const entries = executionModeEntries(todos);
|
|
202
|
+
const { ctx, selectCalls } = createUIContext(entries, "Abort plan");
|
|
203
|
+
|
|
204
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
205
|
+
await harness.fireEvent(
|
|
206
|
+
"agent_end",
|
|
207
|
+
{ messages: [{ role: "assistant", content: [{ type: "text", text: "Finished step 1." }] }] },
|
|
208
|
+
ctx
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(selectCalls).toHaveLength(1);
|
|
212
|
+
expect(selectCalls[0].title).toContain("1/3");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("'Continue execution' sends message with triggerTurn", async () => {
|
|
216
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
217
|
+
const entries = executionModeEntries(todos);
|
|
218
|
+
const { ctx } = createUIContext(entries, "Continue execution");
|
|
219
|
+
|
|
220
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
221
|
+
await harness.fireEvent(
|
|
222
|
+
"agent_end",
|
|
223
|
+
{ messages: [{ role: "assistant", content: [{ type: "text", text: "Partial." }] }] },
|
|
224
|
+
ctx
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const execMsg = harness.sentMessages.find((m) => m.customType === "plan-mode-execute");
|
|
228
|
+
expect(execMsg).toBeDefined();
|
|
229
|
+
expect(execMsg?.options?.triggerTurn).toBe(true);
|
|
230
|
+
expect(execMsg?.content).toContain("step 1");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("'Mark plan as done' clears execution mode and restores tools", async () => {
|
|
234
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
235
|
+
const entries = executionModeEntries(todos);
|
|
236
|
+
const { ctx } = createUIContext(entries, "Mark plan as done");
|
|
237
|
+
|
|
238
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
239
|
+
await harness.fireEvent(
|
|
240
|
+
"agent_end",
|
|
241
|
+
{ messages: [{ role: "assistant", content: [{ type: "text", text: "Done enough." }] }] },
|
|
242
|
+
ctx
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Should send plan-complete message
|
|
246
|
+
const completeMsg = harness.sentMessages.find((m) => m.customType === "plan-complete");
|
|
247
|
+
expect(completeMsg).toBeDefined();
|
|
248
|
+
expect(completeMsg?.options?.triggerTurn).toBe(false);
|
|
249
|
+
|
|
250
|
+
// Should restore full tool set
|
|
251
|
+
expect(harness.api.getActiveTools()).toEqual([...BASELINE_TOOLS]);
|
|
252
|
+
|
|
253
|
+
// Persisted state should reflect cleared execution mode
|
|
254
|
+
const lastEntry = harness.appendedEntries.findLast((e) => e.customType === "plan-mode");
|
|
255
|
+
expect(lastEntry?.data).toMatchObject({ executing: false, todos: [] });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("'Abort plan' clears execution mode without completion message", async () => {
|
|
259
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
260
|
+
const entries = executionModeEntries(todos);
|
|
261
|
+
const { ctx } = createUIContext(entries, "Abort plan");
|
|
262
|
+
|
|
263
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
264
|
+
await harness.fireEvent(
|
|
265
|
+
"agent_end",
|
|
266
|
+
{ messages: [{ role: "assistant", content: [{ type: "text", text: "Stopping." }] }] },
|
|
267
|
+
ctx
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Should NOT send plan-complete message
|
|
271
|
+
const completeMsg = harness.sentMessages.find((m) => m.customType === "plan-complete");
|
|
272
|
+
expect(completeMsg).toBeUndefined();
|
|
273
|
+
|
|
274
|
+
// Should restore full tool set
|
|
275
|
+
expect(harness.api.getActiveTools()).toEqual([...BASELINE_TOOLS]);
|
|
276
|
+
|
|
277
|
+
// Persisted state should reflect cleared execution mode
|
|
278
|
+
const lastEntry = harness.appendedEntries.findLast((e) => e.customType === "plan-mode");
|
|
279
|
+
expect(lastEntry?.data).toMatchObject({ executing: false, todos: [] });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("headless mode (no UI) returns silently", async () => {
|
|
283
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
284
|
+
const entries = executionModeEntries(todos);
|
|
285
|
+
const ctx = createHeadlessContext(entries);
|
|
286
|
+
|
|
287
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
288
|
+
await harness.fireEvent(
|
|
289
|
+
"agent_end",
|
|
290
|
+
{ messages: [{ role: "assistant", content: [{ type: "text", text: "Done." }] }] },
|
|
291
|
+
ctx
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// No select calls, no crash, no sent messages
|
|
295
|
+
expect(harness.sentMessages).toHaveLength(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("all steps completed via [DONE:n] triggers clean completion", async () => {
|
|
299
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
300
|
+
const entries = executionModeEntries(todos);
|
|
301
|
+
const { ctx, selectCalls } = createUIContext(entries);
|
|
302
|
+
|
|
303
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
304
|
+
|
|
305
|
+
// Simulate turn_end with all DONE markers
|
|
306
|
+
await harness.fireEvent(
|
|
307
|
+
"turn_end",
|
|
308
|
+
{
|
|
309
|
+
message: {
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: [{ type: "text", text: "[DONE:1] [DONE:2] [DONE:3]" }],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
ctx
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Now agent_end fires — all steps are complete
|
|
318
|
+
await harness.fireEvent(
|
|
319
|
+
"agent_end",
|
|
320
|
+
{
|
|
321
|
+
messages: [
|
|
322
|
+
{
|
|
323
|
+
role: "assistant",
|
|
324
|
+
content: [{ type: "text", text: "[DONE:1] [DONE:2] [DONE:3]" }],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
ctx
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Should get "Plan Complete!" not the select menu
|
|
332
|
+
const completeMsg = harness.sentMessages.find((m) => m.customType === "plan-complete");
|
|
333
|
+
expect(completeMsg).toBeDefined();
|
|
334
|
+
expect(selectCalls).toHaveLength(0);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("execution mode widgets removed", () => {
|
|
339
|
+
let harness: ExtensionHarness;
|
|
340
|
+
let widgetCalls: Array<{ name: string; value: unknown }>;
|
|
341
|
+
|
|
342
|
+
beforeEach(async () => {
|
|
343
|
+
harness = ExtensionHarness.create();
|
|
344
|
+
await harness.loadExtension(registerMockTools);
|
|
345
|
+
await harness.loadExtension(planModeExtension);
|
|
346
|
+
harness.api.setActiveTools([...BASELINE_TOOLS]);
|
|
347
|
+
widgetCalls = [];
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("execution mode does not render banner or todo widgets", async () => {
|
|
351
|
+
const todos = SAMPLE_TODOS.map((t) => ({ ...t }));
|
|
352
|
+
const entries = executionModeEntries(todos);
|
|
353
|
+
const { ctx } = createUIContext(entries, "Abort plan");
|
|
354
|
+
|
|
355
|
+
// Intercept setWidget calls
|
|
356
|
+
ctx.ui.setWidget = ((name: string, value: unknown) => {
|
|
357
|
+
widgetCalls.push({ name, value });
|
|
358
|
+
}) as never;
|
|
359
|
+
|
|
360
|
+
await harness.fireEvent("session_start", { type: "session_start" }, ctx);
|
|
361
|
+
|
|
362
|
+
// All plan-banner and plan-todos calls should be clearing (undefined)
|
|
363
|
+
const bannerCalls = widgetCalls.filter((c) => c.name === "plan-banner");
|
|
364
|
+
const todoCalls = widgetCalls.filter((c) => c.name === "plan-todos");
|
|
365
|
+
|
|
366
|
+
for (const call of bannerCalls) {
|
|
367
|
+
expect(call.value).toBeUndefined();
|
|
368
|
+
}
|
|
369
|
+
for (const call of todoCalls) {
|
|
370
|
+
expect(call.value).toBeUndefined();
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
@@ -24,12 +24,14 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
type EditorTheme,
|
|
26
26
|
Key,
|
|
27
|
+
Loader,
|
|
27
28
|
type TUI,
|
|
28
29
|
truncateToWidth,
|
|
29
30
|
visibleWidth,
|
|
30
31
|
} from "@mariozechner/pi-tui";
|
|
31
32
|
import { Type } from "@sinclair/typebox";
|
|
32
33
|
import { getIcon } from "../_icons/index.js";
|
|
34
|
+
import { renderBorderedBox } from "../_shared/bordered-box.js";
|
|
33
35
|
import {
|
|
34
36
|
detectPlanIntent,
|
|
35
37
|
extractTodoItems,
|
|
@@ -204,14 +206,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
204
206
|
* @param ctx - The extension context
|
|
205
207
|
*/
|
|
206
208
|
function updateStatus(ctx: ExtensionContext): void {
|
|
207
|
-
// Footer status
|
|
208
|
-
if (
|
|
209
|
-
const completed = todoItems.filter((t) => t.completed).length;
|
|
210
|
-
ctx.ui.setStatus(
|
|
211
|
-
"plan-mode",
|
|
212
|
-
ctx.ui.theme.fg("accent", `${getIcon("task_list")} ${completed}/${todoItems.length}`)
|
|
213
|
-
);
|
|
214
|
-
} else if (planModeEnabled) {
|
|
209
|
+
// Footer status — plan mode only; execution mode defers to tasks extension
|
|
210
|
+
if (planModeEnabled) {
|
|
215
211
|
ctx.ui.setStatus(
|
|
216
212
|
"plan-mode",
|
|
217
213
|
ctx.ui.theme.fg("warning", `${getIcon("plan_mode")} PLAN MODE — read-only`)
|
|
@@ -229,14 +225,15 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
229
225
|
ctx.ui.setEditorComponent(undefined);
|
|
230
226
|
}
|
|
231
227
|
|
|
232
|
-
// Full-width banner above editor
|
|
233
|
-
|
|
228
|
+
// Full-width banner above editor — plan mode only.
|
|
229
|
+
// Execution mode does not show a banner; the tasks extension owns progress tracking.
|
|
230
|
+
if (planModeEnabled) {
|
|
234
231
|
ctx.ui.setWidget("plan-banner", (_tui, theme) => {
|
|
235
|
-
const label =
|
|
236
|
-
const bg = planModeEnabled ? "customMessageBg" : "toolSuccessBg";
|
|
237
|
-
const fg = planModeEnabled ? "customMessageLabel" : "success";
|
|
232
|
+
const label = " PLAN MODE — READ ONLY ";
|
|
238
233
|
return {
|
|
239
|
-
render: (width: number) => [
|
|
234
|
+
render: (width: number) => [
|
|
235
|
+
theme.bg("customMessageBg", theme.fg("customMessageLabel", label.padEnd(width))),
|
|
236
|
+
],
|
|
240
237
|
invalidate() {},
|
|
241
238
|
};
|
|
242
239
|
});
|
|
@@ -244,21 +241,8 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
244
241
|
ctx.ui.setWidget("plan-banner", undefined);
|
|
245
242
|
}
|
|
246
243
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
const lines = todoItems.map((item) => {
|
|
250
|
-
if (item.completed) {
|
|
251
|
-
return (
|
|
252
|
-
ctx.ui.theme.fg("success", "☑ ") +
|
|
253
|
-
ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text))
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
return `${ctx.ui.theme.fg("muted", `${getIcon("pending")} `)}${item.text}`;
|
|
257
|
-
});
|
|
258
|
-
ctx.ui.setWidget("plan-todos", lines);
|
|
259
|
-
} else {
|
|
260
|
-
ctx.ui.setWidget("plan-todos", undefined);
|
|
261
|
-
}
|
|
244
|
+
// Todo list widget — plan mode only (execution delegates to tasks extension)
|
|
245
|
+
ctx.ui.setWidget("plan-todos", undefined);
|
|
262
246
|
}
|
|
263
247
|
|
|
264
248
|
/**
|
|
@@ -636,7 +620,77 @@ If you receive [PLAN GUIDANCE — Step n: ...], treat it as user steering for th
|
|
|
636
620
|
currentStepIndex = null;
|
|
637
621
|
restoreNormalModeTools();
|
|
638
622
|
updateStatus(ctx);
|
|
639
|
-
persistState();
|
|
623
|
+
persistState();
|
|
624
|
+
} else if (ctx.hasUI) {
|
|
625
|
+
// Agent finished its turn but not all steps are marked complete.
|
|
626
|
+
// Show progress summary and prompt for next action.
|
|
627
|
+
const completed = todoItems.filter((t) => t.completed);
|
|
628
|
+
|
|
629
|
+
updateStatus(ctx);
|
|
630
|
+
ctx.ui.setWorkingMessage(Loader.HIDE);
|
|
631
|
+
|
|
632
|
+
const choice = await ctx.ui.select(
|
|
633
|
+
`Plan execution paused (${completed.length}/${todoItems.length} done)`,
|
|
634
|
+
["Continue execution", "Provide guidance", "Mark plan as done", "Abort plan"]
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (choice === "Continue execution") {
|
|
638
|
+
const nextStep = getCurrentStep();
|
|
639
|
+
const continueMessage =
|
|
640
|
+
nextStep !== null
|
|
641
|
+
? `Continue executing the plan. Next: step ${nextStep.step}: ${nextStep.text}`
|
|
642
|
+
: "Continue executing the remaining plan steps.";
|
|
643
|
+
pi.sendMessage(
|
|
644
|
+
{ customType: "plan-mode-execute", content: continueMessage, display: true },
|
|
645
|
+
{ triggerTurn: true }
|
|
646
|
+
);
|
|
647
|
+
} else if (choice === "Provide guidance") {
|
|
648
|
+
const nextStep = getCurrentStep();
|
|
649
|
+
const stepLabel = nextStep ? `step ${nextStep.step} (${nextStep.text})` : "next step";
|
|
650
|
+
const guidance = await ctx.ui.editor(`Guidance for ${stepLabel}:`, "");
|
|
651
|
+
const trimmedGuidance = guidance?.trim();
|
|
652
|
+
if (trimmedGuidance && nextStep) {
|
|
653
|
+
pi.sendUserMessage(
|
|
654
|
+
[
|
|
655
|
+
`[PLAN GUIDANCE — Step ${nextStep.step}: ${nextStep.text}]`,
|
|
656
|
+
"",
|
|
657
|
+
"User guidance:",
|
|
658
|
+
trimmedGuidance,
|
|
659
|
+
].join("\n")
|
|
660
|
+
);
|
|
661
|
+
} else if (trimmedGuidance) {
|
|
662
|
+
pi.sendUserMessage(trimmedGuidance);
|
|
663
|
+
} else {
|
|
664
|
+
ctx.ui.notify("No guidance provided. Plan unchanged.", "info");
|
|
665
|
+
}
|
|
666
|
+
} else if (choice === "Mark plan as done") {
|
|
667
|
+
const completedList = todoItems
|
|
668
|
+
.map((t) => (t.completed ? `~~${t.text}~~` : t.text))
|
|
669
|
+
.join("\n");
|
|
670
|
+
pi.sendMessage(
|
|
671
|
+
{
|
|
672
|
+
customType: "plan-complete",
|
|
673
|
+
content: `**Plan Complete!** ${getIcon("success")}\n\n${completedList}`,
|
|
674
|
+
display: true,
|
|
675
|
+
},
|
|
676
|
+
{ triggerTurn: false }
|
|
677
|
+
);
|
|
678
|
+
executionMode = false;
|
|
679
|
+
todoItems = [];
|
|
680
|
+
currentStepIndex = null;
|
|
681
|
+
restoreNormalModeTools();
|
|
682
|
+
updateStatus(ctx);
|
|
683
|
+
persistState();
|
|
684
|
+
} else {
|
|
685
|
+
// Abort plan (or dismissed/escaped the select)
|
|
686
|
+
executionMode = false;
|
|
687
|
+
todoItems = [];
|
|
688
|
+
currentStepIndex = null;
|
|
689
|
+
restoreNormalModeTools();
|
|
690
|
+
updateStatus(ctx);
|
|
691
|
+
persistState();
|
|
692
|
+
ctx.ui.notify("Plan aborted.", "info");
|
|
693
|
+
}
|
|
640
694
|
}
|
|
641
695
|
return;
|
|
642
696
|
}
|
|
@@ -652,27 +706,35 @@ If you receive [PLAN GUIDANCE — Step n: ...], treat it as user steering for th
|
|
|
652
706
|
}
|
|
653
707
|
}
|
|
654
708
|
|
|
655
|
-
// Show plan steps
|
|
709
|
+
// Show plan steps in a bordered widget above the editor
|
|
656
710
|
if (todoItems.length > 0) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
711
|
+
ctx.ui.setWidget("plan-steps", (_tui, theme) => ({
|
|
712
|
+
render(width: number): string[] {
|
|
713
|
+
const stepLines = todoItems.map(
|
|
714
|
+
(t) => `${theme.fg("muted", `${getIcon("pending")} `)}${t.text}`
|
|
715
|
+
);
|
|
716
|
+
return renderBorderedBox(stepLines, width, {
|
|
717
|
+
title: `PLAN (${todoItems.length} steps)`,
|
|
718
|
+
style: "rounded",
|
|
719
|
+
borderColorFn: (s: string) => theme.fg("warning", s),
|
|
720
|
+
titleColorFn: (s: string) => theme.fg("warning", s),
|
|
721
|
+
});
|
|
665
722
|
},
|
|
666
|
-
{
|
|
667
|
-
);
|
|
723
|
+
invalidate() {},
|
|
724
|
+
}));
|
|
668
725
|
}
|
|
669
726
|
|
|
727
|
+
ctx.ui.setWorkingMessage(Loader.HIDE);
|
|
728
|
+
|
|
670
729
|
const choice = await ctx.ui.select("Plan mode - what next?", [
|
|
671
730
|
todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan",
|
|
672
731
|
"Stay in plan mode",
|
|
673
732
|
"Refine the plan",
|
|
674
733
|
]);
|
|
675
734
|
|
|
735
|
+
// Clear the plan steps widget after user makes a choice
|
|
736
|
+
ctx.ui.setWidget("plan-steps", undefined);
|
|
737
|
+
|
|
676
738
|
if (choice?.startsWith("Execute")) {
|
|
677
739
|
planModeEnabled = false;
|
|
678
740
|
executionMode = todoItems.length > 0;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for prompt-suggestions editor capability detection.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolvePromptSuggestionEditor } from "../index.js";
|
|
7
|
+
|
|
8
|
+
describe("resolvePromptSuggestionEditor", () => {
|
|
9
|
+
test("returns editor when ghost-text APIs are available", () => {
|
|
10
|
+
const editor = {
|
|
11
|
+
addChangeListener() {},
|
|
12
|
+
getText() {
|
|
13
|
+
return "hello";
|
|
14
|
+
},
|
|
15
|
+
setGhostText() {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
expect(resolvePromptSuggestionEditor(editor)).toBe(editor);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns null when setGhostText is missing", () => {
|
|
22
|
+
const editor = {
|
|
23
|
+
addChangeListener() {},
|
|
24
|
+
getText() {
|
|
25
|
+
return "hello";
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(resolvePromptSuggestionEditor(editor)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns null when addChangeListener is missing", () => {
|
|
33
|
+
const editor = {
|
|
34
|
+
getText() {
|
|
35
|
+
return "hello";
|
|
36
|
+
},
|
|
37
|
+
setGhostText() {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
expect(resolvePromptSuggestionEditor(editor)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -46,6 +46,31 @@ const DEFAULT_DEBOUNCE_MS = 600;
|
|
|
46
46
|
/** Max autocomplete calls per session (cost guardrail). */
|
|
47
47
|
const MAX_CALLS_PER_SESSION = 200;
|
|
48
48
|
|
|
49
|
+
interface PromptSuggestionEditor {
|
|
50
|
+
addChangeListener(fn: (text: string) => void): void;
|
|
51
|
+
getText(): string;
|
|
52
|
+
setGhostText(text: string | null): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the subset of editor APIs required by prompt suggestions.
|
|
57
|
+
*
|
|
58
|
+
* Published installs may resolve upstream `@mariozechner/pi-tui`, whose
|
|
59
|
+
* editor does not implement ghost-text helpers. In that case, the
|
|
60
|
+
* extension should degrade safely instead of crashing.
|
|
61
|
+
*
|
|
62
|
+
* @param editor - Candidate editor instance
|
|
63
|
+
* @returns Prompt-suggestion editor surface, or null when unavailable
|
|
64
|
+
*/
|
|
65
|
+
export function resolvePromptSuggestionEditor(editor: unknown): PromptSuggestionEditor | null {
|
|
66
|
+
if (!editor || typeof editor !== "object") return null;
|
|
67
|
+
const candidate = editor as Partial<PromptSuggestionEditor>;
|
|
68
|
+
if (typeof candidate.getText !== "function") return null;
|
|
69
|
+
if (typeof candidate.setGhostText !== "function") return null;
|
|
70
|
+
if (typeof candidate.addChangeListener !== "function") return null;
|
|
71
|
+
return candidate as PromptSuggestionEditor;
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
// ─── Idle suggestions ────────────────────────────────────────────────────────
|
|
50
75
|
|
|
51
76
|
/**
|
|
@@ -214,7 +239,7 @@ export default function promptSuggestions(pi: ExtensionAPI): void {
|
|
|
214
239
|
const modelSetting = readSetting("prompt-suggestions.model", DEFAULT_AUTOCOMPLETE_MODEL);
|
|
215
240
|
|
|
216
241
|
/** Reference to the editor instance for ghost text control. */
|
|
217
|
-
let editorRef:
|
|
242
|
+
let editorRef: PromptSuggestionEditor | null = null;
|
|
218
243
|
|
|
219
244
|
/** Autocomplete engine instance, created after editor is available. */
|
|
220
245
|
let engine: AutocompleteEngine | null = null;
|
|
@@ -275,8 +300,18 @@ export default function promptSuggestions(pi: ExtensionAPI): void {
|
|
|
275
300
|
|
|
276
301
|
ctx.ui.setEditorComponent((tui, editorTheme: EditorTheme, keybindings) => {
|
|
277
302
|
const editor = new CustomEditor(tui, editorTheme, keybindings);
|
|
278
|
-
|
|
279
|
-
|
|
303
|
+
const promptEditor = resolvePromptSuggestionEditor(editor);
|
|
304
|
+
if (!promptEditor) {
|
|
305
|
+
engine = null;
|
|
306
|
+
editorRef = null;
|
|
307
|
+
ctx.ui.notify(
|
|
308
|
+
"Prompt suggestions disabled: current editor runtime lacks ghost-text support.",
|
|
309
|
+
"warning"
|
|
310
|
+
);
|
|
311
|
+
return editor;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
editorRef = promptEditor;
|
|
280
315
|
engine = new AutocompleteEngine(
|
|
281
316
|
{
|
|
282
317
|
enabled: autocompleteEnabled,
|
|
@@ -286,12 +321,12 @@ export default function promptSuggestions(pi: ExtensionAPI): void {
|
|
|
286
321
|
},
|
|
287
322
|
ctx.modelRegistry,
|
|
288
323
|
getCompletion,
|
|
289
|
-
(text) =>
|
|
290
|
-
() =>
|
|
324
|
+
(text) => promptEditor.setGhostText(text),
|
|
325
|
+
() => promptEditor.getText(),
|
|
291
326
|
() => (sessionManagerRef ? buildConversationContext(sessionManagerRef) : null)
|
|
292
327
|
);
|
|
293
328
|
|
|
294
|
-
|
|
329
|
+
promptEditor.addChangeListener((newText: string) => {
|
|
295
330
|
clearIdleTimer();
|
|
296
331
|
|
|
297
332
|
if (newText.length === 0 && !engine?.busy) {
|