@onefl666/dsh-great-plan 0.0.0-stage → 0.5.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.
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/README.zh-CN.md +100 -0
- package/client.actions.js +1031 -0
- package/client.js +378 -0
- package/client.model.js +232 -0
- package/client.panel.js +897 -0
- package/client.preset.js +127 -0
- package/client.settings.js +226 -0
- package/client.ui.js +534 -0
- package/cordis.patch.yml +3 -0
- package/host/apply.js +24 -0
- package/host/config.js +19 -0
- package/host/plan-compaction.js +96 -0
- package/host/plan-evidence.js +252 -0
- package/host/plan-range.js +32 -0
- package/icon.svg +19 -0
- package/index.js +2 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +68 -4
package/client.panel.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@onefl666/dsh-great-plan',
|
|
3
|
+
chunk: 'client.panel.js',
|
|
4
|
+
factory(require) {
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
const ui = require('@onefl666/dsh-great-plan/client.ui.js');
|
|
8
|
+
const model = require('@onefl666/dsh-great-plan/client.model.js');
|
|
9
|
+
const preset = require('@onefl666/dsh-great-plan/client.preset.js');
|
|
10
|
+
const actionHelpers = require('@onefl666/dsh-great-plan/client.actions.js');
|
|
11
|
+
|
|
12
|
+
const { useEffect, useMemo, useRef, useState } = React;
|
|
13
|
+
|
|
14
|
+
/** Every fresh-session prompt switch defaults to on, like the Host Config. */
|
|
15
|
+
const DEFAULT_SWITCHES = {
|
|
16
|
+
unlimitedBudget: true,
|
|
17
|
+
includeOriginalInstructions: true,
|
|
18
|
+
includeMinimalContext: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Instructions state before the clear view reads the old session. */
|
|
22
|
+
function emptyInstructions() {
|
|
23
|
+
return { status: 'idle', texts: [], count: 0, chars: 0 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Normalize the injected `readPromptSwitches` face; unreadable values stay on. */
|
|
27
|
+
function normalizeSwitches(read) {
|
|
28
|
+
if (typeof actionHelpers.normalizePromptSwitches === 'function') {
|
|
29
|
+
try {
|
|
30
|
+
return actionHelpers.normalizePromptSwitches(read);
|
|
31
|
+
} catch {
|
|
32
|
+
// Fall through to the local defaults.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = typeof read === 'function' ? read() : undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
value = undefined;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'boolean') {
|
|
42
|
+
return { unlimitedBudget: value, includeOriginalInstructions: true, includeMinimalContext: true };
|
|
43
|
+
}
|
|
44
|
+
if (value !== null && typeof value === 'object') {
|
|
45
|
+
return {
|
|
46
|
+
unlimitedBudget: value.unlimitedBudget !== false,
|
|
47
|
+
includeOriginalInstructions: value.includeOriginalInstructions !== false,
|
|
48
|
+
includeMinimalContext: value.includeMinimalContext !== false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { ...DEFAULT_SWITCHES };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read the current runtime facts for the preview; execution re-reads them. */
|
|
55
|
+
function readRuntimeFactsForPreview() {
|
|
56
|
+
if (typeof actionHelpers.readRuntimeFacts === 'function') {
|
|
57
|
+
try {
|
|
58
|
+
const facts = actionHelpers.readRuntimeFacts();
|
|
59
|
+
if (facts !== null && typeof facts === 'object') return facts;
|
|
60
|
+
} catch {
|
|
61
|
+
// Fall through to the empty facts.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { system: undefined, time: undefined };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Localize an ActionError when the actions module knows the code. */
|
|
68
|
+
function describeActionErrorSafe(t, caught) {
|
|
69
|
+
if (typeof actionHelpers.describeActionError === 'function') {
|
|
70
|
+
try {
|
|
71
|
+
const described = actionHelpers.describeActionError(t, caught);
|
|
72
|
+
if (typeof described === 'string' && described !== '') return described;
|
|
73
|
+
} catch {
|
|
74
|
+
// Fall through to the contextual fallback.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Localized error text: the shared ActionError copy, else the panel fallback. */
|
|
81
|
+
function describeCaught(t, caught, fallbackKey) {
|
|
82
|
+
return describeActionErrorSafe(t, caught) ?? t(fallbackKey, { message: ui.messageOf(caught) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Localize one compaction failure message; unknown text passes through. */
|
|
86
|
+
function compactFailureMessage(t, failure) {
|
|
87
|
+
if (typeof actionHelpers.describeCompactFailure === 'function') {
|
|
88
|
+
try {
|
|
89
|
+
const described = actionHelpers.describeCompactFailure(t, failure.message);
|
|
90
|
+
if (typeof described === 'string' && described !== '') return described;
|
|
91
|
+
} catch {
|
|
92
|
+
// Fall through to the raw message.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return failure.message;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Plan summary/full-length pair; never throws for an odd review. */
|
|
99
|
+
function planInfoOf(review) {
|
|
100
|
+
if (typeof actionHelpers.planPreviewText === 'function') {
|
|
101
|
+
try {
|
|
102
|
+
const info = actionHelpers.planPreviewText(review?.plan);
|
|
103
|
+
if (info !== null && typeof info === 'object') {
|
|
104
|
+
return {
|
|
105
|
+
preview: typeof info.preview === 'string' ? info.preview : '',
|
|
106
|
+
chars: typeof info.chars === 'number' ? info.chars : 0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Fall through to the empty info.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { preview: '', chars: 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Full plan text; empty when the review carries none. */
|
|
117
|
+
function planTextOf(review) {
|
|
118
|
+
return typeof review?.plan === 'string' ? review.plan : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The fresh-message preview; degraded shapes stay renderable. */
|
|
122
|
+
function freshPreviewOf(input) {
|
|
123
|
+
if (typeof actionHelpers.describeFreshPrompt === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
const preview = actionHelpers.describeFreshPrompt(input);
|
|
126
|
+
if (preview !== null && typeof preview === 'object') return preview;
|
|
127
|
+
} catch {
|
|
128
|
+
// Fall through to the empty preview.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
text: '',
|
|
133
|
+
blocks: [],
|
|
134
|
+
instructionCount: 0,
|
|
135
|
+
instructionChars: 0,
|
|
136
|
+
totalChars: 0,
|
|
137
|
+
approxTokens: 0,
|
|
138
|
+
oversized: false,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Approved-plan section shared by the root and clear views.
|
|
144
|
+
*
|
|
145
|
+
* The plan heading is a real h3 and the expanded pre becomes a labelled
|
|
146
|
+
* region; the toggle only exists when the review carries plan text.
|
|
147
|
+
*/
|
|
148
|
+
function planSectionOf({ review, t, expanded, onToggle }) {
|
|
149
|
+
const info = planInfoOf(review);
|
|
150
|
+
const body = planTextOf(review);
|
|
151
|
+
const question = typeof review?.question === 'string' ? review.question.trim() : '';
|
|
152
|
+
const expandable = body.trim() !== '';
|
|
153
|
+
return h('div', {
|
|
154
|
+
className: 'gp-plan',
|
|
155
|
+
role: expanded && expandable ? 'region' : undefined,
|
|
156
|
+
'aria-label': expanded && expandable ? t('preview.plan') : undefined,
|
|
157
|
+
},
|
|
158
|
+
h('h3', { className: 'gp-plan-heading' }, t('preview.plan')),
|
|
159
|
+
question === '' ? null : h('p', { className: 'gp-plan-question' }, question),
|
|
160
|
+
info.preview === '' ? null : h('p', { className: 'gp-plan-preview' }, info.preview),
|
|
161
|
+
expandable ? h(ui.Button, {
|
|
162
|
+
variant: 'ghost',
|
|
163
|
+
className: 'gp-plan-toggle',
|
|
164
|
+
'aria-expanded': expanded,
|
|
165
|
+
onClick: onToggle,
|
|
166
|
+
}, expanded ? t('preview.hidePlan') : t('preview.viewPlan')) : null,
|
|
167
|
+
expanded && expandable
|
|
168
|
+
? h('pre', { className: 'gp-plan-body' }, body)
|
|
169
|
+
: null,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ExecutionTrigger(props) {
|
|
174
|
+
const {
|
|
175
|
+
review,
|
|
176
|
+
requestKey,
|
|
177
|
+
directory,
|
|
178
|
+
actions,
|
|
179
|
+
presetStore,
|
|
180
|
+
sessionId,
|
|
181
|
+
useSessionStatus,
|
|
182
|
+
useSessions,
|
|
183
|
+
useWorkspaces,
|
|
184
|
+
t,
|
|
185
|
+
modelAvailable,
|
|
186
|
+
readPromptSwitches,
|
|
187
|
+
} = props;
|
|
188
|
+
const pending = useSessionStatus((snapshot) => snapshot.get(sessionId)?.pendingInteraction);
|
|
189
|
+
const row = useSessions((snapshot) => snapshot.byId[sessionId]);
|
|
190
|
+
const workspaces = useWorkspaces((snapshot) => snapshot);
|
|
191
|
+
const [open, setOpen] = useState(false);
|
|
192
|
+
const triggerRef = useRef(null);
|
|
193
|
+
const workspace = useMemo(
|
|
194
|
+
() => workspaces.items?.find((item) => item.sessionIds?.includes(sessionId)),
|
|
195
|
+
[workspaces, sessionId],
|
|
196
|
+
);
|
|
197
|
+
const workspaceId = workspace?.workspaceId;
|
|
198
|
+
const workspaceDirectory = workspace?.path ?? row?.cwd;
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
setOpen(false);
|
|
202
|
+
}, [requestKey]);
|
|
203
|
+
|
|
204
|
+
if (pending === undefined || pending.kind !== 'plan-review') return null;
|
|
205
|
+
|
|
206
|
+
return h(React.Fragment, null,
|
|
207
|
+
h('button', {
|
|
208
|
+
ref: triggerRef,
|
|
209
|
+
type: 'button',
|
|
210
|
+
className: 'gp-trigger',
|
|
211
|
+
'aria-haspopup': 'dialog',
|
|
212
|
+
'aria-expanded': open,
|
|
213
|
+
'aria-label': t('trigger.aria'),
|
|
214
|
+
title: t('trigger.aria'),
|
|
215
|
+
onClick: () => setOpen(true),
|
|
216
|
+
},
|
|
217
|
+
h(ui.IconSliders, { className: 'gp-trigger-icon' }),
|
|
218
|
+
h('span', { className: 'gp-trigger-label' }, t('trigger.label')),
|
|
219
|
+
h(ui.IconChevronDown, { className: 'gp-trigger-chevron' }),
|
|
220
|
+
),
|
|
221
|
+
open
|
|
222
|
+
? h(ExecutionDialog, {
|
|
223
|
+
open,
|
|
224
|
+
onClose: () => setOpen(false),
|
|
225
|
+
triggerRef,
|
|
226
|
+
pending,
|
|
227
|
+
review,
|
|
228
|
+
requestKey,
|
|
229
|
+
directory,
|
|
230
|
+
actions,
|
|
231
|
+
presetStore,
|
|
232
|
+
sessionId,
|
|
233
|
+
workspaceId,
|
|
234
|
+
workspaceDirectory,
|
|
235
|
+
cwd: row?.cwd,
|
|
236
|
+
title: row?.title,
|
|
237
|
+
currentPreset: row?.projectionValues?.agentPreset ?? undefined,
|
|
238
|
+
modelAvailable,
|
|
239
|
+
readPromptSwitches,
|
|
240
|
+
t,
|
|
241
|
+
})
|
|
242
|
+
: null,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ExecutionDialog(props) {
|
|
247
|
+
const {
|
|
248
|
+
open,
|
|
249
|
+
onClose,
|
|
250
|
+
triggerRef,
|
|
251
|
+
pending,
|
|
252
|
+
review,
|
|
253
|
+
directory,
|
|
254
|
+
actions,
|
|
255
|
+
presetStore,
|
|
256
|
+
sessionId,
|
|
257
|
+
workspaceId,
|
|
258
|
+
workspaceDirectory,
|
|
259
|
+
cwd,
|
|
260
|
+
title,
|
|
261
|
+
currentPreset,
|
|
262
|
+
modelAvailable,
|
|
263
|
+
readPromptSwitches,
|
|
264
|
+
t,
|
|
265
|
+
} = props;
|
|
266
|
+
const directoryState = model.useDirectoryState(directory);
|
|
267
|
+
const presetState = preset.usePresetState(presetStore.store);
|
|
268
|
+
const [view, setView] = useState('root');
|
|
269
|
+
const [busy, setBusy] = useState(null);
|
|
270
|
+
const [error, setError] = useState(null);
|
|
271
|
+
const [cleanupWarning, setCleanupWarning] = useState(null);
|
|
272
|
+
const [compactFailure, setCompactFailure] = useState(null);
|
|
273
|
+
const [statusLine, setStatusLine] = useState(null);
|
|
274
|
+
const [presetChoice, setPresetChoice] = useState(null);
|
|
275
|
+
const [runSwitches, setRunSwitches] = useState(null);
|
|
276
|
+
const [previewFacts, setPreviewFacts] = useState(null);
|
|
277
|
+
const [instructionsState, setInstructionsState] = useState(emptyInstructions);
|
|
278
|
+
const [planExpanded, setPlanExpanded] = useState(false);
|
|
279
|
+
const aliveRef = useRef(true);
|
|
280
|
+
const abortRef = useRef(null);
|
|
281
|
+
const instructionsAbortRef = useRef(null);
|
|
282
|
+
const titleRef = useRef(null);
|
|
283
|
+
const previousViewRef = useRef(view);
|
|
284
|
+
const pendingRef = useRef(pending);
|
|
285
|
+
pendingRef.current = pending;
|
|
286
|
+
|
|
287
|
+
useEffect(() => () => {
|
|
288
|
+
aliveRef.current = false;
|
|
289
|
+
abortRef.current?.abort();
|
|
290
|
+
instructionsAbortRef.current?.abort();
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (!open) {
|
|
295
|
+
setView('root');
|
|
296
|
+
setBusy(null);
|
|
297
|
+
setError(null);
|
|
298
|
+
setCleanupWarning(null);
|
|
299
|
+
setCompactFailure(null);
|
|
300
|
+
setStatusLine(null);
|
|
301
|
+
setPresetChoice(null);
|
|
302
|
+
setRunSwitches(null);
|
|
303
|
+
setPreviewFacts(null);
|
|
304
|
+
setInstructionsState(emptyInstructions());
|
|
305
|
+
setPlanExpanded(false);
|
|
306
|
+
abortRef.current?.abort();
|
|
307
|
+
instructionsAbortRef.current?.abort();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
directory.load().catch(() => {});
|
|
311
|
+
}, [open, directory]);
|
|
312
|
+
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (!open || view !== 'clear') return;
|
|
315
|
+
presetStore.load().catch(() => {});
|
|
316
|
+
}, [open, view, presetStore]);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (previousViewRef.current === view) return;
|
|
320
|
+
previousViewRef.current = view;
|
|
321
|
+
const node = titleRef.current;
|
|
322
|
+
if (node !== null && node !== undefined && typeof node.focus === 'function') node.focus();
|
|
323
|
+
}, [view]);
|
|
324
|
+
|
|
325
|
+
const options = presetState.options ?? [];
|
|
326
|
+
const defaultPresetId = preset.defaultPresetId(presetState);
|
|
327
|
+
const currentPresetAvailable = currentPreset !== undefined && options.some((option) => option.id === currentPreset);
|
|
328
|
+
const effectivePresetId = presetChoice ?? (currentPresetAvailable ? currentPreset : defaultPresetId);
|
|
329
|
+
const selectedPreset = options.find((option) => option.id === effectivePresetId);
|
|
330
|
+
const presetLabel = selectedPreset === undefined
|
|
331
|
+
? (effectivePresetId ?? t('preset.default'))
|
|
332
|
+
: preset.presetDisplay(selectedPreset, t);
|
|
333
|
+
const previewTitle = actions.composeFreshTitle(title, t('clear.titleSuffix'));
|
|
334
|
+
const modelSelecting = directoryState.status === 'selecting';
|
|
335
|
+
const controlBusy = busy !== null || modelSelecting;
|
|
336
|
+
const blocked = busy === 'model'
|
|
337
|
+
|| busy === 'effort'
|
|
338
|
+
|| busy === 'execute'
|
|
339
|
+
|| busy === 'clear'
|
|
340
|
+
|| modelSelecting;
|
|
341
|
+
const canRevise = typeof pending?.dismiss === 'function' && !blocked;
|
|
342
|
+
const switches = runSwitches ?? normalizeSwitches(readPromptSwitches);
|
|
343
|
+
|
|
344
|
+
const start = (kind) => {
|
|
345
|
+
const controller = new AbortController();
|
|
346
|
+
abortRef.current = controller;
|
|
347
|
+
setBusy(kind);
|
|
348
|
+
setError(null);
|
|
349
|
+
setCleanupWarning(null);
|
|
350
|
+
setStatusLine(null);
|
|
351
|
+
return controller;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const finish = () => {
|
|
355
|
+
if (aliveRef.current) setBusy(null);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const close = () => {
|
|
359
|
+
if (blocked) return;
|
|
360
|
+
abortRef.current?.abort();
|
|
361
|
+
instructionsAbortRef.current?.abort();
|
|
362
|
+
onClose();
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const startInstructionsRead = () => {
|
|
366
|
+
if (typeof actions.readPlanModeInstructions !== 'function') {
|
|
367
|
+
setInstructionsState({ status: 'unavailable', texts: [], count: 0, chars: 0 });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
instructionsAbortRef.current?.abort();
|
|
371
|
+
const controller = new AbortController();
|
|
372
|
+
instructionsAbortRef.current = controller;
|
|
373
|
+
setInstructionsState({ status: 'loading', texts: [], count: 0, chars: 0 });
|
|
374
|
+
let read;
|
|
375
|
+
try {
|
|
376
|
+
read = actions.readPlanModeInstructions({ signal: controller.signal });
|
|
377
|
+
} catch {
|
|
378
|
+
read = undefined;
|
|
379
|
+
}
|
|
380
|
+
Promise.resolve(read).then(
|
|
381
|
+
(result) => {
|
|
382
|
+
if (!aliveRef.current || controller.signal.aborted) return;
|
|
383
|
+
const texts = Array.isArray(result?.texts)
|
|
384
|
+
? result.texts.filter((text) => typeof text === 'string' && text.trim() !== '')
|
|
385
|
+
: [];
|
|
386
|
+
const chars = texts.reduce((sum, text) => sum + text.length, 0);
|
|
387
|
+
setInstructionsState({
|
|
388
|
+
status: texts.length > 0 ? 'ready' : 'unavailable',
|
|
389
|
+
texts,
|
|
390
|
+
count: texts.length,
|
|
391
|
+
chars,
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
() => {
|
|
395
|
+
if (!aliveRef.current || controller.signal.aborted) return;
|
|
396
|
+
setInstructionsState({ status: 'unavailable', texts: [], count: 0, chars: 0 });
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const enterClear = () => {
|
|
402
|
+
if (blocked || controlBusy) return;
|
|
403
|
+
setError(null);
|
|
404
|
+
setStatusLine(null);
|
|
405
|
+
setCompactFailure(null);
|
|
406
|
+
setCleanupWarning(null);
|
|
407
|
+
setPreviewFacts(readRuntimeFactsForPreview());
|
|
408
|
+
const initial = normalizeSwitches(readPromptSwitches);
|
|
409
|
+
setRunSwitches(initial);
|
|
410
|
+
setInstructionsState(emptyInstructions());
|
|
411
|
+
if (initial.includeOriginalInstructions) startInstructionsRead();
|
|
412
|
+
setView('clear');
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const leaveClear = () => {
|
|
416
|
+
instructionsAbortRef.current?.abort();
|
|
417
|
+
instructionsAbortRef.current = null;
|
|
418
|
+
setInstructionsState(emptyInstructions());
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const toggleSwitch = (name) => {
|
|
422
|
+
const current = runSwitches ?? normalizeSwitches(readPromptSwitches);
|
|
423
|
+
const next = { ...current, [name]: !current[name] };
|
|
424
|
+
setRunSwitches(next);
|
|
425
|
+
if (name === 'includeOriginalInstructions'
|
|
426
|
+
&& next.includeOriginalInstructions
|
|
427
|
+
&& instructionsState.status !== 'ready'
|
|
428
|
+
&& instructionsState.status !== 'loading') {
|
|
429
|
+
startInstructionsRead();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const selectModel = async (group, item) => {
|
|
434
|
+
if (busy !== null) return;
|
|
435
|
+
setBusy('model');
|
|
436
|
+
setError(null);
|
|
437
|
+
try {
|
|
438
|
+
const result = await directory.select({ provider: group.id, model: item.id });
|
|
439
|
+
if (!aliveRef.current) return;
|
|
440
|
+
if (!result.ok) {
|
|
441
|
+
setError(t('error.selectModelFailed', { message: `${result.error.message} (${result.error.code})` }));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
setView('root');
|
|
445
|
+
} catch (caught) {
|
|
446
|
+
if (aliveRef.current) setError(t('error.selectModelFailed', { message: ui.messageOf(caught) }));
|
|
447
|
+
} finally {
|
|
448
|
+
finish();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const selectEffort = async (effort) => {
|
|
453
|
+
if (busy !== null || directoryState.current === null || directoryState.current === undefined) return;
|
|
454
|
+
setBusy('effort');
|
|
455
|
+
setError(null);
|
|
456
|
+
try {
|
|
457
|
+
const result = await directory.select({
|
|
458
|
+
provider: directoryState.current.provider,
|
|
459
|
+
model: directoryState.current.model,
|
|
460
|
+
...effort === undefined ? {} : { reasoningEffort: effort },
|
|
461
|
+
});
|
|
462
|
+
if (!aliveRef.current) return;
|
|
463
|
+
if (!result.ok) {
|
|
464
|
+
setError(t('error.selectModelFailed', { message: `${result.error.message} (${result.error.code})` }));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
setView('root');
|
|
468
|
+
} catch (caught) {
|
|
469
|
+
if (aliveRef.current) setError(t('error.selectModelFailed', { message: ui.messageOf(caught) }));
|
|
470
|
+
} finally {
|
|
471
|
+
finish();
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const execute = async () => {
|
|
476
|
+
if (busy !== null) return;
|
|
477
|
+
if (pending === undefined) {
|
|
478
|
+
setError(t('error.reviewGone'));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const controller = start('execute');
|
|
482
|
+
try {
|
|
483
|
+
await actions.approveReview({ pending, review });
|
|
484
|
+
if (aliveRef.current) onClose();
|
|
485
|
+
} catch (caught) {
|
|
486
|
+
if (aliveRef.current && !controller.signal.aborted) {
|
|
487
|
+
setError(describeCaught(t, caught, 'error.approveFailed'));
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
finish();
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const proceedAnyway = async () => {
|
|
495
|
+
if (busy !== null) return;
|
|
496
|
+
if (pending === undefined) {
|
|
497
|
+
setError(t('error.reviewGone'));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const controller = start('execute');
|
|
501
|
+
try {
|
|
502
|
+
await actions.approveReview({ pending, review });
|
|
503
|
+
if (aliveRef.current) onClose();
|
|
504
|
+
} catch (caught) {
|
|
505
|
+
if (aliveRef.current && !controller.signal.aborted) {
|
|
506
|
+
setError(describeCaught(t, caught, 'error.approveFailed'));
|
|
507
|
+
}
|
|
508
|
+
} finally {
|
|
509
|
+
finish();
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const compact = async () => {
|
|
514
|
+
if (busy !== null) return;
|
|
515
|
+
if (pending === undefined) {
|
|
516
|
+
setError(t('error.reviewGone'));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const controller = start('compact');
|
|
520
|
+
try {
|
|
521
|
+
let result;
|
|
522
|
+
try {
|
|
523
|
+
result = await actions.runCompactCommand({ signal: controller.signal });
|
|
524
|
+
} catch (caught) {
|
|
525
|
+
result = { ok: false, message: ui.messageOf(caught) };
|
|
526
|
+
}
|
|
527
|
+
if (!aliveRef.current || controller.signal.aborted) return;
|
|
528
|
+
if (!result.ok) {
|
|
529
|
+
setCompactFailure({ message: result.message, code: result.code, raw: result.raw });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
setCompactFailure(null);
|
|
533
|
+
setStatusLine(t('compact.success'));
|
|
534
|
+
try {
|
|
535
|
+
await actions.approveReview({ pending, review });
|
|
536
|
+
if (aliveRef.current) onClose();
|
|
537
|
+
} catch (caught) {
|
|
538
|
+
if (aliveRef.current && !controller.signal.aborted) {
|
|
539
|
+
setStatusLine(null);
|
|
540
|
+
setError(describeCaught(t, caught, 'error.approveFailed'));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} finally {
|
|
544
|
+
finish();
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const cancelCompactFailure = () => {
|
|
549
|
+
setCompactFailure(null);
|
|
550
|
+
setStatusLine(null);
|
|
551
|
+
close();
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const revise = async () => {
|
|
555
|
+
if (busy !== null) return;
|
|
556
|
+
setBusy('dismiss');
|
|
557
|
+
setError(null);
|
|
558
|
+
setCleanupWarning(null);
|
|
559
|
+
setStatusLine(null);
|
|
560
|
+
setCompactFailure(null);
|
|
561
|
+
try {
|
|
562
|
+
await actions.dismissReview({ pending });
|
|
563
|
+
if (aliveRef.current) onClose();
|
|
564
|
+
} catch (caught) {
|
|
565
|
+
if (aliveRef.current) setError(describeCaught(t, caught, 'error.dismissFailed'));
|
|
566
|
+
} finally {
|
|
567
|
+
finish();
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const clearExecute = async () => {
|
|
572
|
+
if (busy !== null) return;
|
|
573
|
+
if (pending === undefined) {
|
|
574
|
+
setError(t('error.reviewGone'));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const activeSwitches = runSwitches ?? normalizeSwitches(readPromptSwitches);
|
|
578
|
+
if (activeSwitches.includeOriginalInstructions && instructionsState.status === 'loading') return;
|
|
579
|
+
const controller = start('clear');
|
|
580
|
+
try {
|
|
581
|
+
const result = await actions.createFreshExecution({
|
|
582
|
+
review,
|
|
583
|
+
presetId: effectivePresetId,
|
|
584
|
+
workspaceId,
|
|
585
|
+
workspaceDirectory,
|
|
586
|
+
cwd,
|
|
587
|
+
selection: model.toModelSelection(directoryState.current),
|
|
588
|
+
title,
|
|
589
|
+
titleSuffix: t('clear.titleSuffix'),
|
|
590
|
+
signal: controller.signal,
|
|
591
|
+
t,
|
|
592
|
+
instructions: activeSwitches.includeOriginalInstructions ? instructionsState.texts : undefined,
|
|
593
|
+
switches: activeSwitches,
|
|
594
|
+
stillPending: () => pendingRef.current !== undefined,
|
|
595
|
+
});
|
|
596
|
+
if (!aliveRef.current) return;
|
|
597
|
+
if (result?.cancelError !== undefined) {
|
|
598
|
+
console.warn('great-plan: the previous session was not stopped:', result.cancelError);
|
|
599
|
+
}
|
|
600
|
+
onClose();
|
|
601
|
+
} catch (caught) {
|
|
602
|
+
if (aliveRef.current && !controller.signal.aborted) {
|
|
603
|
+
setError(describeCaught(t, caught, 'error.freshFailed'));
|
|
604
|
+
const cleanup = caught?.cleanupError;
|
|
605
|
+
if (typeof cleanup === 'string' && cleanup !== '') {
|
|
606
|
+
setCleanupWarning(t('error.cleanupFailed', { message: cleanup }));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} finally {
|
|
610
|
+
finish();
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const back = () => {
|
|
615
|
+
if (blocked) return;
|
|
616
|
+
setError(null);
|
|
617
|
+
setStatusLine(null);
|
|
618
|
+
setCompactFailure(null);
|
|
619
|
+
setCleanupWarning(null);
|
|
620
|
+
if (view === 'preset') {
|
|
621
|
+
setView('clear');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (view === 'clear') leaveClear();
|
|
625
|
+
setView('root');
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const titles = {
|
|
629
|
+
root: t('panel.title'),
|
|
630
|
+
model: t('panel.model'),
|
|
631
|
+
effort: t('panel.effort'),
|
|
632
|
+
clear: t('clear.title'),
|
|
633
|
+
preset: t('preset.title'),
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
let subtitle;
|
|
637
|
+
if (view === 'model' || view === 'effort') subtitle = model.summaryOf(directoryState, t);
|
|
638
|
+
else if (view === 'root') subtitle = t('panel.subtitle');
|
|
639
|
+
|
|
640
|
+
const header = h('div', { className: 'gp-dialog-header' },
|
|
641
|
+
view === 'root' ? null : h(ui.IconButton, {
|
|
642
|
+
label: t('panel.back'),
|
|
643
|
+
onClick: back,
|
|
644
|
+
disabled: blocked,
|
|
645
|
+
}, h(ui.IconChevronLeft)),
|
|
646
|
+
h('div', { className: 'gp-dialog-title-wrap' },
|
|
647
|
+
h('h2', {
|
|
648
|
+
ref: titleRef,
|
|
649
|
+
className: 'gp-dialog-title',
|
|
650
|
+
id: 'gp-great-plan-title',
|
|
651
|
+
tabIndex: -1,
|
|
652
|
+
title: titles[view],
|
|
653
|
+
}, titles[view]),
|
|
654
|
+
subtitle === undefined ? null : h('div', { className: 'gp-dialog-subtitle' }, subtitle),
|
|
655
|
+
),
|
|
656
|
+
h(ui.IconButton, {
|
|
657
|
+
label: t('panel.close'),
|
|
658
|
+
onClick: close,
|
|
659
|
+
disabled: blocked,
|
|
660
|
+
autoFocus: true,
|
|
661
|
+
}, h(ui.IconClose)),
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
const planSection = planSectionOf({
|
|
665
|
+
review,
|
|
666
|
+
t,
|
|
667
|
+
expanded: planExpanded,
|
|
668
|
+
onToggle: () => setPlanExpanded((value) => !value),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
let body;
|
|
672
|
+
if (view === 'model') {
|
|
673
|
+
body = h('div', { className: 'gp-dialog-body' },
|
|
674
|
+
error === null ? null : h(ui.ErrorText, null, error),
|
|
675
|
+
h(model.ModelListView, {
|
|
676
|
+
state: directoryState,
|
|
677
|
+
t,
|
|
678
|
+
disabled: controlBusy,
|
|
679
|
+
onPick: selectModel,
|
|
680
|
+
onRetry: () => directory.load().catch(() => {}),
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
} else if (view === 'effort') {
|
|
684
|
+
body = h('div', { className: 'gp-dialog-body' },
|
|
685
|
+
error === null ? null : h(ui.ErrorText, null, error),
|
|
686
|
+
h(model.EffortListView, {
|
|
687
|
+
state: directoryState,
|
|
688
|
+
t,
|
|
689
|
+
disabled: controlBusy,
|
|
690
|
+
onPick: selectEffort,
|
|
691
|
+
}),
|
|
692
|
+
);
|
|
693
|
+
} else if (view === 'preset') {
|
|
694
|
+
body = h('div', { className: 'gp-dialog-body' },
|
|
695
|
+
h(preset.PresetOptions, {
|
|
696
|
+
state: presetState,
|
|
697
|
+
t,
|
|
698
|
+
selectedId: effectivePresetId,
|
|
699
|
+
onPick: (id) => {
|
|
700
|
+
setPresetChoice(id);
|
|
701
|
+
setView('clear');
|
|
702
|
+
},
|
|
703
|
+
onRetry: () => presetStore.load(true).catch(() => {}),
|
|
704
|
+
}),
|
|
705
|
+
);
|
|
706
|
+
} else if (view === 'clear') {
|
|
707
|
+
const preview = freshPreviewOf({
|
|
708
|
+
plan: review?.plan,
|
|
709
|
+
t,
|
|
710
|
+
presetId: effectivePresetId,
|
|
711
|
+
instructions: instructionsState.texts,
|
|
712
|
+
workspaceDirectory,
|
|
713
|
+
runtimeFacts: previewFacts ?? undefined,
|
|
714
|
+
switches,
|
|
715
|
+
});
|
|
716
|
+
const instructionsOn = switches.includeOriginalInstructions === true;
|
|
717
|
+
const instructionsLoading = instructionsOn && instructionsState.status === 'loading';
|
|
718
|
+
const directoryLoading = directoryState.status === 'loading' || directoryState.status === 'idle';
|
|
719
|
+
const directoryError = directoryState.status === 'error';
|
|
720
|
+
const minimalDisabled = effectivePresetId !== 'minimal';
|
|
721
|
+
const runtimeIncluded = switches.includeMinimalContext === true && effectivePresetId === 'minimal';
|
|
722
|
+
const confirmDisabled = controlBusy || directoryLoading || instructionsLoading;
|
|
723
|
+
body = h('div', { className: 'gp-dialog-body' },
|
|
724
|
+
planSection,
|
|
725
|
+
h(ui.WarningBox, null, t('clear.warning')),
|
|
726
|
+
h(ui.SelectRow, {
|
|
727
|
+
label: t('clear.preset'),
|
|
728
|
+
value: presetLabel,
|
|
729
|
+
disabled: controlBusy,
|
|
730
|
+
onClick: () => setView('preset'),
|
|
731
|
+
}),
|
|
732
|
+
h(ui.StaticRow, { label: t('clear.titleLabel'), value: previewTitle ?? t('clear.autoTitle') }),
|
|
733
|
+
h(ui.StaticRow, { label: t('clear.model'), value: model.summaryOf(directoryState, t) }),
|
|
734
|
+
h(ui.Note, null, t('clear.modelHint')),
|
|
735
|
+
h('div', { className: 'gp-plan' },
|
|
736
|
+
h('h3', { className: 'gp-plan-heading' }, t('preview.title')),
|
|
737
|
+
h('div', { className: 'gp-note' }, t('preview.blocks')),
|
|
738
|
+
(preview.blocks ?? []).map((block, index) => h('div', {
|
|
739
|
+
key: block?.id ?? `block-${index}`,
|
|
740
|
+
className: 'gp-static-row',
|
|
741
|
+
},
|
|
742
|
+
h('span', { className: 'gp-static-label' }, t(block?.titleKey ?? 'preview.title')),
|
|
743
|
+
h('span', { className: 'gp-static-value' }, block?.included === false
|
|
744
|
+
? t('preview.excluded')
|
|
745
|
+
: `${t('preview.included')} · ${typeof block?.chars === 'number' ? block.chars : 0}`),
|
|
746
|
+
)),
|
|
747
|
+
instructionsOn
|
|
748
|
+
? (
|
|
749
|
+
instructionsLoading
|
|
750
|
+
? h(ui.Note, null, h(ui.Loading, null, t('preview.loadingInstructions')))
|
|
751
|
+
: instructionsState.status === 'ready'
|
|
752
|
+
? h(ui.Note, null, t('preview.instructionCount', {
|
|
753
|
+
count: instructionsState.count,
|
|
754
|
+
chars: instructionsState.chars,
|
|
755
|
+
}))
|
|
756
|
+
: h(ui.Note, null, t('preview.instructionsUnavailable'))
|
|
757
|
+
)
|
|
758
|
+
: h(ui.Note, null, t('preview.noInstructions')),
|
|
759
|
+
h(ui.Note, null, t('preview.total', { chars: preview.totalChars ?? 0 })),
|
|
760
|
+
h(ui.Note, null, t('preview.approxTokens', { tokens: preview.approxTokens ?? 0 })),
|
|
761
|
+
preview.oversized === true
|
|
762
|
+
? h(ui.WarningText, null, t('preview.oversized', { chars: preview.totalChars ?? 0 }))
|
|
763
|
+
: null,
|
|
764
|
+
runtimeIncluded ? h(ui.Note, null, t('preview.runtimeHint')) : null,
|
|
765
|
+
),
|
|
766
|
+
h('div', { className: 'gp-checks', role: 'group', 'aria-label': t('preview.switches') },
|
|
767
|
+
h('div', { className: 'gp-check-heading' }, t('preview.switches')),
|
|
768
|
+
h(ui.Checkbox, {
|
|
769
|
+
label: t('preview.switchBudget'),
|
|
770
|
+
checked: switches.unlimitedBudget !== false,
|
|
771
|
+
disabled: controlBusy,
|
|
772
|
+
onChange: () => toggleSwitch('unlimitedBudget'),
|
|
773
|
+
}),
|
|
774
|
+
h(ui.Checkbox, {
|
|
775
|
+
label: t('preview.switchInstructions'),
|
|
776
|
+
checked: instructionsOn,
|
|
777
|
+
disabled: controlBusy,
|
|
778
|
+
onChange: () => toggleSwitch('includeOriginalInstructions'),
|
|
779
|
+
}),
|
|
780
|
+
h(ui.Checkbox, {
|
|
781
|
+
label: t('preview.switchMinimal'),
|
|
782
|
+
checked: switches.includeMinimalContext !== false,
|
|
783
|
+
disabled: controlBusy || minimalDisabled,
|
|
784
|
+
hint: minimalDisabled ? t('preview.minimalSwitchHint') : undefined,
|
|
785
|
+
onChange: () => toggleSwitch('includeMinimalContext'),
|
|
786
|
+
}),
|
|
787
|
+
),
|
|
788
|
+
directoryLoading ? h(ui.Note, null, t('panel.modelDirectoryLoading')) : null,
|
|
789
|
+
directoryError ? h(ui.Note, null, t('panel.modelDirectoryError')) : null,
|
|
790
|
+
cleanupWarning === null ? null : h(ui.WarningText, null, cleanupWarning),
|
|
791
|
+
error === null ? null : h(ui.ErrorText, null, error),
|
|
792
|
+
h('div', { className: 'gp-clear-actions' },
|
|
793
|
+
h(ui.Button, {
|
|
794
|
+
variant: 'primary',
|
|
795
|
+
full: true,
|
|
796
|
+
disabled: confirmDisabled,
|
|
797
|
+
onClick: clearExecute,
|
|
798
|
+
}, busy === 'clear' ? h(ui.Loading, null, t('clear.busy')) : t('clear.confirm')),
|
|
799
|
+
),
|
|
800
|
+
h(ui.Note, { className: 'gp-footer-note' }, t('clear.kept')),
|
|
801
|
+
);
|
|
802
|
+
} else {
|
|
803
|
+
body = h('div', { className: 'gp-dialog-body' },
|
|
804
|
+
planSection,
|
|
805
|
+
h(model.ModelRows, {
|
|
806
|
+
state: directoryState,
|
|
807
|
+
t,
|
|
808
|
+
disabled: !modelAvailable || controlBusy,
|
|
809
|
+
onOpenModel: () => setView('model'),
|
|
810
|
+
onOpenEffort: () => setView('effort'),
|
|
811
|
+
onRetry: () => directory.load().catch(() => {}),
|
|
812
|
+
}),
|
|
813
|
+
!modelAvailable ? h(ui.Note, null, t('panel.subagentUnavailable')) : null,
|
|
814
|
+
compactFailure === null
|
|
815
|
+
? null
|
|
816
|
+
: h('div', { className: 'gp-failure' },
|
|
817
|
+
h(ui.WarningBox, null, t('compact.failed', { message: compactFailureMessage(t, compactFailure) })),
|
|
818
|
+
h(ui.Note, null, t('compact.failedHint')),
|
|
819
|
+
h('div', { className: 'gp-failure-actions' },
|
|
820
|
+
h(ui.Button, {
|
|
821
|
+
variant: 'outline',
|
|
822
|
+
disabled: controlBusy,
|
|
823
|
+
onClick: compact,
|
|
824
|
+
}, busy === 'compact' ? h(ui.Loading, null, t('compact.retrying')) : t('compact.retry')),
|
|
825
|
+
h(ui.Button, {
|
|
826
|
+
variant: 'primary',
|
|
827
|
+
disabled: controlBusy,
|
|
828
|
+
onClick: proceedAnyway,
|
|
829
|
+
}, t('compact.proceed')),
|
|
830
|
+
h(ui.Button, {
|
|
831
|
+
variant: 'ghost',
|
|
832
|
+
disabled: controlBusy,
|
|
833
|
+
onClick: cancelCompactFailure,
|
|
834
|
+
}, t('compact.cancel')),
|
|
835
|
+
),
|
|
836
|
+
),
|
|
837
|
+
statusLine === null ? null : h('div', { className: 'gp-status', role: 'status' }, statusLine),
|
|
838
|
+
error === null ? null : h(ui.ErrorText, null, error),
|
|
839
|
+
compactFailure === null
|
|
840
|
+
? h('div', null,
|
|
841
|
+
h('div', { className: 'gp-separator' }),
|
|
842
|
+
h('div', { className: 'gp-actions' },
|
|
843
|
+
h(ui.ActionRow, {
|
|
844
|
+
primary: true,
|
|
845
|
+
icon: h(ui.IconPlay),
|
|
846
|
+
title: t('panel.actions.execute.title'),
|
|
847
|
+
description: t('panel.actions.execute.description'),
|
|
848
|
+
disabled: controlBusy,
|
|
849
|
+
onClick: execute,
|
|
850
|
+
}),
|
|
851
|
+
h(ui.ActionRow, {
|
|
852
|
+
icon: h(ui.IconCompress),
|
|
853
|
+
title: t('panel.actions.compact.title'),
|
|
854
|
+
description: t('panel.actions.compact.description'),
|
|
855
|
+
disabled: controlBusy,
|
|
856
|
+
onClick: compact,
|
|
857
|
+
}),
|
|
858
|
+
h(ui.ActionRow, {
|
|
859
|
+
icon: h(ui.IconNewSession),
|
|
860
|
+
title: t('panel.actions.clear.title'),
|
|
861
|
+
description: t('panel.actions.clear.description'),
|
|
862
|
+
disabled: controlBusy || !modelAvailable,
|
|
863
|
+
onClick: enterClear,
|
|
864
|
+
}),
|
|
865
|
+
),
|
|
866
|
+
)
|
|
867
|
+
: null,
|
|
868
|
+
canRevise
|
|
869
|
+
? h('div', { className: 'gp-actions' },
|
|
870
|
+
h(ui.Button, {
|
|
871
|
+
variant: 'ghost',
|
|
872
|
+
full: true,
|
|
873
|
+
disabled: controlBusy,
|
|
874
|
+
onClick: revise,
|
|
875
|
+
}, busy === 'dismiss' ? h(ui.Loading, null, t('panel.loading')) : t('panel.revise')),
|
|
876
|
+
)
|
|
877
|
+
: null,
|
|
878
|
+
h(ui.Note, { className: 'gp-footer-note' }, t('panel.applyNote')),
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return h(ui.Dialog, {
|
|
883
|
+
open,
|
|
884
|
+
onRequestClose: close,
|
|
885
|
+
blocked,
|
|
886
|
+
busy: busy !== null,
|
|
887
|
+
anchorRef: triggerRef,
|
|
888
|
+
labelledBy: 'gp-great-plan-title',
|
|
889
|
+
}, header, body);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
ExecutionTrigger,
|
|
894
|
+
ExecutionDialog,
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
});
|