@onefl666/dsh-great-plan 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,749 @@
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
+ /** Instructions state before the clear view reads the old session. */
15
+ function emptyInstructions() {
16
+ return { status: 'idle', texts: [], count: 0, chars: 0 };
17
+ }
18
+
19
+ /** The fresh-message preview; the shared assembler throws ActionError('emptyPlan') for an empty plan. */
20
+ function freshPreviewOf(input) {
21
+ try {
22
+ return actionHelpers.describeFreshPrompt(input);
23
+ } catch {
24
+ return {
25
+ text: '',
26
+ blocks: [],
27
+ instructionCount: 0,
28
+ instructionChars: 0,
29
+ totalChars: 0,
30
+ approxTokens: 0,
31
+ oversized: false,
32
+ };
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Approved-plan section shared by the root and clear views.
38
+ *
39
+ * The plan heading is a real h3 and the expanded pre becomes a labelled
40
+ * region; the toggle only exists when the review carries plan text.
41
+ */
42
+ function planSectionOf({ review, t, expanded, onToggle }) {
43
+ const info = actionHelpers.planPreviewText(review?.plan);
44
+ const body = typeof review?.plan === 'string' ? review.plan : '';
45
+ const question = typeof review?.question === 'string' ? review.question.trim() : '';
46
+ const expandable = body.trim() !== '';
47
+ return h('div', {
48
+ className: 'gp-plan',
49
+ role: expanded && expandable ? 'region' : undefined,
50
+ 'aria-label': expanded && expandable ? t('preview.plan') : undefined,
51
+ },
52
+ h('h3', { className: 'gp-plan-heading' }, t('preview.plan')),
53
+ question === '' ? null : h('p', { className: 'gp-plan-question' }, question),
54
+ info.preview === '' ? null : h('p', { className: 'gp-plan-preview' }, info.preview),
55
+ expandable ? h(ui.Button, {
56
+ variant: 'ghost',
57
+ className: 'gp-plan-toggle',
58
+ 'aria-expanded': expanded,
59
+ onClick: onToggle,
60
+ }, expanded ? t('preview.hidePlan') : t('preview.viewPlan')) : null,
61
+ expanded && expandable
62
+ ? h('pre', { className: 'gp-plan-body' }, body)
63
+ : null,
64
+ );
65
+ }
66
+
67
+ function ExecutionTrigger(props) {
68
+ const {
69
+ review,
70
+ requestKey,
71
+ directory,
72
+ actions,
73
+ presetStore,
74
+ sessionId,
75
+ useSessionStatus,
76
+ useSessions,
77
+ useWorkspaces,
78
+ t,
79
+ modelAvailable,
80
+ readPromptSwitches,
81
+ } = props;
82
+ const pending = useSessionStatus((snapshot) => snapshot.get(sessionId)?.pendingInteraction);
83
+ const row = useSessions((snapshot) => snapshot.byId[sessionId]);
84
+ const workspaces = useWorkspaces((snapshot) => snapshot);
85
+ const [open, setOpen] = useState(false);
86
+ const triggerRef = useRef(null);
87
+ const workspace = useMemo(
88
+ () => workspaces.items?.find((item) => item.sessionIds?.includes(sessionId)),
89
+ [workspaces, sessionId],
90
+ );
91
+ const workspaceId = workspace?.workspaceId;
92
+ const workspaceDirectory = workspace?.path ?? row?.cwd;
93
+
94
+ useEffect(() => {
95
+ setOpen(false);
96
+ }, [requestKey]);
97
+
98
+ if (pending === undefined || pending.kind !== 'plan-review') return null;
99
+
100
+ return h(React.Fragment, null,
101
+ h('button', {
102
+ ref: triggerRef,
103
+ type: 'button',
104
+ className: 'gp-trigger',
105
+ 'aria-haspopup': 'dialog',
106
+ 'aria-expanded': open,
107
+ 'aria-label': t('trigger.aria'),
108
+ title: t('trigger.aria'),
109
+ onClick: () => setOpen(true),
110
+ },
111
+ h(ui.IconSliders),
112
+ h('span', { className: 'gp-trigger-label' }, t('trigger.label')),
113
+ h(ui.IconChevronDown, { className: 'gp-trigger-chevron' }),
114
+ ),
115
+ open
116
+ ? h(ExecutionDialog, {
117
+ open,
118
+ onClose: () => setOpen(false),
119
+ triggerRef,
120
+ pending,
121
+ review,
122
+ directory,
123
+ actions,
124
+ presetStore,
125
+ sessionId,
126
+ workspaceId,
127
+ workspaceDirectory,
128
+ cwd: row?.cwd,
129
+ title: row?.title,
130
+ currentPreset: row?.projectionValues?.agentPreset ?? undefined,
131
+ modelAvailable,
132
+ readPromptSwitches,
133
+ t,
134
+ })
135
+ : null,
136
+ );
137
+ }
138
+
139
+ function ExecutionDialog(props) {
140
+ const {
141
+ open,
142
+ onClose,
143
+ triggerRef,
144
+ pending,
145
+ review,
146
+ directory,
147
+ actions,
148
+ presetStore,
149
+ sessionId,
150
+ workspaceId,
151
+ workspaceDirectory,
152
+ cwd,
153
+ title,
154
+ currentPreset,
155
+ modelAvailable,
156
+ readPromptSwitches,
157
+ t,
158
+ } = props;
159
+ const directoryState = model.useDirectoryState(directory);
160
+ const presetState = ui.useStore(presetStore.store);
161
+ const [view, setView] = useState('root');
162
+ const [busy, setBusy] = useState(null);
163
+ const [error, setError] = useState(null);
164
+ const [cleanupWarning, setCleanupWarning] = useState(null);
165
+ const [compactFailure, setCompactFailure] = useState(null);
166
+ const [statusLine, setStatusLine] = useState(null);
167
+ const [presetChoice, setPresetChoice] = useState(null);
168
+ const [runSwitches, setRunSwitches] = useState(null);
169
+ const [previewFacts, setPreviewFacts] = useState(null);
170
+ const [instructionsState, setInstructionsState] = useState(emptyInstructions);
171
+ const [planExpanded, setPlanExpanded] = useState(false);
172
+ const aliveRef = useRef(true);
173
+ const abortRef = useRef(null);
174
+ const instructionsAbortRef = useRef(null);
175
+ const titleRef = useRef(null);
176
+ const previousViewRef = useRef(view);
177
+ const pendingRef = useRef(pending);
178
+ pendingRef.current = pending;
179
+
180
+ useEffect(() => () => {
181
+ aliveRef.current = false;
182
+ abortRef.current?.abort();
183
+ instructionsAbortRef.current?.abort();
184
+ }, []);
185
+
186
+ useEffect(() => {
187
+ directory.load().catch(() => {});
188
+ }, [open, directory]);
189
+
190
+ useEffect(() => {
191
+ if (!open || view !== 'clear') return;
192
+ presetStore.load().catch(() => {});
193
+ }, [open, view, presetStore]);
194
+
195
+ useEffect(() => {
196
+ if (previousViewRef.current === view) return;
197
+ previousViewRef.current = view;
198
+ const node = titleRef.current;
199
+ if (node !== null && node !== undefined && typeof node.focus === 'function') node.focus();
200
+ }, [view]);
201
+
202
+ const options = presetState.options ?? [];
203
+ const defaultPresetId = preset.defaultPresetId(presetState);
204
+ const currentPresetAvailable = currentPreset !== undefined && options.some((option) => option.id === currentPreset);
205
+ const effectivePresetId = presetChoice ?? (currentPresetAvailable ? currentPreset : defaultPresetId);
206
+ const selectedPreset = options.find((option) => option.id === effectivePresetId);
207
+ const presetLabel = selectedPreset === undefined
208
+ ? (effectivePresetId ?? t('preset.default'))
209
+ : preset.presetDisplay(selectedPreset, t);
210
+ const previewTitle = actions.composeFreshTitle(title, t('clear.titleSuffix'));
211
+ const modelSelecting = directoryState.status === 'selecting';
212
+ const controlBusy = busy !== null || modelSelecting;
213
+ const blocked = busy === 'model'
214
+ || busy === 'effort'
215
+ || busy === 'execute'
216
+ || busy === 'clear'
217
+ || modelSelecting;
218
+ const canRevise = typeof pending?.dismiss === 'function' && !blocked;
219
+ const switches = runSwitches ?? actionHelpers.normalizePromptSwitches(readPromptSwitches);
220
+
221
+ const start = (kind) => {
222
+ const controller = new AbortController();
223
+ abortRef.current = controller;
224
+ setBusy(kind);
225
+ setError(null);
226
+ setCleanupWarning(null);
227
+ setStatusLine(null);
228
+ return controller;
229
+ };
230
+
231
+ const finish = () => {
232
+ if (aliveRef.current) setBusy(null);
233
+ };
234
+
235
+ const close = () => {
236
+ if (blocked) return;
237
+ abortRef.current?.abort();
238
+ instructionsAbortRef.current?.abort();
239
+ onClose();
240
+ };
241
+
242
+ const startInstructionsRead = () => {
243
+ if (typeof actions.readPlanModeInstructions !== 'function') {
244
+ setInstructionsState({ status: 'unavailable', texts: [], count: 0, chars: 0 });
245
+ return;
246
+ }
247
+ instructionsAbortRef.current?.abort();
248
+ const controller = new AbortController();
249
+ instructionsAbortRef.current = controller;
250
+ setInstructionsState({ status: 'loading', texts: [], count: 0, chars: 0 });
251
+ let read;
252
+ try {
253
+ read = actions.readPlanModeInstructions({ signal: controller.signal });
254
+ } catch {
255
+ read = undefined;
256
+ }
257
+ Promise.resolve(read).then(
258
+ (result) => {
259
+ if (!aliveRef.current || controller.signal.aborted) return;
260
+ const texts = Array.isArray(result?.texts)
261
+ ? result.texts.filter((text) => typeof text === 'string' && text.trim() !== '')
262
+ : [];
263
+ const chars = texts.reduce((sum, text) => sum + text.length, 0);
264
+ setInstructionsState({
265
+ status: texts.length > 0 ? 'ready' : 'unavailable',
266
+ texts,
267
+ count: texts.length,
268
+ chars,
269
+ });
270
+ },
271
+ () => {
272
+ if (!aliveRef.current || controller.signal.aborted) return;
273
+ setInstructionsState({ status: 'unavailable', texts: [], count: 0, chars: 0 });
274
+ },
275
+ );
276
+ };
277
+
278
+ const enterClear = () => {
279
+ if (blocked || controlBusy) return;
280
+ setError(null);
281
+ setStatusLine(null);
282
+ setCompactFailure(null);
283
+ setCleanupWarning(null);
284
+ setPreviewFacts(actionHelpers.readRuntimeFacts());
285
+ const initial = actionHelpers.normalizePromptSwitches(readPromptSwitches);
286
+ setRunSwitches(initial);
287
+ setInstructionsState(emptyInstructions());
288
+ if (initial.includeOriginalInstructions) startInstructionsRead();
289
+ setView('clear');
290
+ };
291
+
292
+ const leaveClear = () => {
293
+ instructionsAbortRef.current?.abort();
294
+ instructionsAbortRef.current = null;
295
+ setInstructionsState(emptyInstructions());
296
+ };
297
+
298
+ const toggleSwitch = (name) => {
299
+ const current = runSwitches ?? actionHelpers.normalizePromptSwitches(readPromptSwitches);
300
+ const next = { ...current, [name]: !current[name] };
301
+ setRunSwitches(next);
302
+ if (name === 'includeOriginalInstructions'
303
+ && next.includeOriginalInstructions
304
+ && instructionsState.status !== 'ready'
305
+ && instructionsState.status !== 'loading') {
306
+ startInstructionsRead();
307
+ }
308
+ };
309
+
310
+ const applySelection = async (kind, payload) => {
311
+ if (busy !== null) return;
312
+ if (kind === 'effort' && (directoryState.current === null || directoryState.current === undefined)) return;
313
+ setBusy(kind);
314
+ setError(null);
315
+ try {
316
+ const result = await directory.select(payload);
317
+ if (!aliveRef.current) return;
318
+ if (!result.ok) {
319
+ setError(t('error.selectModelFailed', { message: actionHelpers.formatRpcFailure(result) }));
320
+ return;
321
+ }
322
+ setView('root');
323
+ } catch (caught) {
324
+ if (aliveRef.current) setError(t('error.selectModelFailed', { message: ui.messageOf(caught) }));
325
+ } finally {
326
+ finish();
327
+ }
328
+ };
329
+
330
+ const approvePending = async () => {
331
+ if (busy !== null) return;
332
+ if (pending === undefined) {
333
+ setError(t('error.reviewGone'));
334
+ return;
335
+ }
336
+ const controller = start('execute');
337
+ try {
338
+ await actions.approveReview({ pending, review });
339
+ if (aliveRef.current) onClose();
340
+ } catch (caught) {
341
+ if (aliveRef.current && !controller.signal.aborted) {
342
+ setError(actionHelpers.describeActionError(t, caught) ?? t('error.approveFailed', { message: ui.messageOf(caught) }));
343
+ }
344
+ } finally {
345
+ finish();
346
+ }
347
+ };
348
+
349
+ const compact = async () => {
350
+ if (busy !== null) return;
351
+ if (pending === undefined) {
352
+ setError(t('error.reviewGone'));
353
+ return;
354
+ }
355
+ const controller = start('compact');
356
+ try {
357
+ let result;
358
+ try {
359
+ result = await actions.runCompactCommand({ signal: controller.signal });
360
+ } catch (caught) {
361
+ result = { ok: false, message: ui.messageOf(caught) };
362
+ }
363
+ if (!aliveRef.current || controller.signal.aborted) return;
364
+ if (!result.ok) {
365
+ setCompactFailure({ message: result.message, code: result.code, raw: result.raw });
366
+ return;
367
+ }
368
+ setCompactFailure(null);
369
+ setStatusLine(t('compact.success'));
370
+ try {
371
+ await actions.approveReview({ pending, review });
372
+ if (aliveRef.current) onClose();
373
+ } catch (caught) {
374
+ if (aliveRef.current && !controller.signal.aborted) {
375
+ setStatusLine(null);
376
+ setError(actionHelpers.describeActionError(t, caught) ?? t('error.approveFailed', { message: ui.messageOf(caught) }));
377
+ }
378
+ }
379
+ } finally {
380
+ finish();
381
+ }
382
+ };
383
+
384
+ const cancelCompactFailure = () => {
385
+ setCompactFailure(null);
386
+ setStatusLine(null);
387
+ close();
388
+ };
389
+
390
+ const revise = async () => {
391
+ if (busy !== null) return;
392
+ setBusy('dismiss');
393
+ setError(null);
394
+ setCleanupWarning(null);
395
+ setStatusLine(null);
396
+ setCompactFailure(null);
397
+ try {
398
+ await actions.dismissReview({ pending });
399
+ if (aliveRef.current) onClose();
400
+ } catch (caught) {
401
+ if (aliveRef.current) setError(actionHelpers.describeActionError(t, caught) ?? t('error.dismissFailed', { message: ui.messageOf(caught) }));
402
+ } finally {
403
+ finish();
404
+ }
405
+ };
406
+
407
+ const clearExecute = async () => {
408
+ if (busy !== null) return;
409
+ if (pending === undefined) {
410
+ setError(t('error.reviewGone'));
411
+ return;
412
+ }
413
+ const activeSwitches = runSwitches ?? actionHelpers.normalizePromptSwitches(readPromptSwitches);
414
+ if (activeSwitches.includeOriginalInstructions && instructionsState.status === 'loading') return;
415
+ const controller = start('clear');
416
+ try {
417
+ const result = await actions.createFreshExecution({
418
+ review,
419
+ presetId: effectivePresetId,
420
+ workspaceId,
421
+ workspaceDirectory,
422
+ cwd,
423
+ selection: model.toModelSelection(directoryState.current),
424
+ title,
425
+ titleSuffix: t('clear.titleSuffix'),
426
+ signal: controller.signal,
427
+ instructions: activeSwitches.includeOriginalInstructions ? instructionsState.texts : undefined,
428
+ switches: activeSwitches,
429
+ stillPending: () => pendingRef.current !== undefined,
430
+ });
431
+ if (!aliveRef.current) return;
432
+ if (result?.cancelError !== undefined) {
433
+ console.warn('great-plan: the previous session was not stopped:', result.cancelError);
434
+ }
435
+ onClose();
436
+ } catch (caught) {
437
+ if (aliveRef.current && !controller.signal.aborted) {
438
+ setError(actionHelpers.describeActionError(t, caught) ?? t('error.freshFailed', { message: ui.messageOf(caught) }));
439
+ const cleanup = caught?.cleanupError;
440
+ if (typeof cleanup === 'string' && cleanup !== '') {
441
+ setCleanupWarning(t('error.cleanupFailed', { message: cleanup }));
442
+ }
443
+ }
444
+ } finally {
445
+ finish();
446
+ }
447
+ };
448
+
449
+ const back = () => {
450
+ if (blocked) return;
451
+ setError(null);
452
+ setStatusLine(null);
453
+ setCompactFailure(null);
454
+ setCleanupWarning(null);
455
+ if (view === 'preset') {
456
+ setView('clear');
457
+ return;
458
+ }
459
+ if (view === 'clear') leaveClear();
460
+ setView('root');
461
+ };
462
+
463
+ const titles = {
464
+ root: t('panel.title'),
465
+ model: t('panel.model'),
466
+ effort: t('panel.effort'),
467
+ clear: t('clear.title'),
468
+ preset: t('preset.title'),
469
+ };
470
+
471
+ const renderHeader = () => {
472
+ let subtitle;
473
+ if (view === 'model' || view === 'effort') subtitle = model.summaryOf(directoryState, t);
474
+ else if (view === 'root') subtitle = t('panel.subtitle');
475
+ return h('div', { className: 'gp-dialog-header' },
476
+ view === 'root' ? null : h(ui.IconButton, {
477
+ label: t('panel.back'),
478
+ onClick: back,
479
+ disabled: blocked,
480
+ }, h(ui.IconChevronLeft)),
481
+ h('div', { className: 'gp-dialog-title-wrap' },
482
+ h('h2', {
483
+ ref: titleRef,
484
+ className: 'gp-dialog-title',
485
+ id: 'gp-great-plan-title',
486
+ tabIndex: -1,
487
+ title: titles[view],
488
+ }, titles[view]),
489
+ subtitle === undefined ? null : h('div', { className: 'gp-dialog-subtitle' }, subtitle),
490
+ ),
491
+ h(ui.IconButton, {
492
+ label: t('panel.close'),
493
+ onClick: close,
494
+ disabled: blocked,
495
+ autoFocus: true,
496
+ }, h(ui.IconClose)),
497
+ );
498
+ };
499
+
500
+ const planSection = planSectionOf({
501
+ review,
502
+ t,
503
+ expanded: planExpanded,
504
+ onToggle: () => setPlanExpanded((value) => !value),
505
+ });
506
+
507
+ const renderCompactFailure = () => compactFailure === null
508
+ ? null
509
+ : h('div', { className: 'gp-failure' },
510
+ h(ui.WarningBox, null, t('compact.failed', { message: actionHelpers.describeCompactFailure(t, compactFailure.message) })),
511
+ h(ui.Note, null, t('compact.failedHint')),
512
+ h('div', { className: 'gp-failure-actions' },
513
+ h(ui.Button, {
514
+ variant: 'outline',
515
+ disabled: controlBusy,
516
+ onClick: compact,
517
+ }, busy === 'compact' ? h(ui.Loading, null, t('compact.retrying')) : t('compact.retry')),
518
+ h(ui.Button, {
519
+ variant: 'primary',
520
+ disabled: controlBusy,
521
+ onClick: approvePending,
522
+ }, t('compact.proceed')),
523
+ h(ui.Button, {
524
+ variant: 'ghost',
525
+ disabled: controlBusy,
526
+ onClick: cancelCompactFailure,
527
+ }, t('compact.cancel')),
528
+ ),
529
+ );
530
+
531
+ const renderModelView = () => h('div', { className: 'gp-dialog-body' },
532
+ error === null ? null : h(ui.ErrorText, null, error),
533
+ h(model.ModelListView, {
534
+ state: directoryState,
535
+ t,
536
+ disabled: controlBusy,
537
+ onPick: (group, item) => applySelection('model', { provider: group.id, model: item.id }),
538
+ onRetry: () => directory.load().catch(() => {}),
539
+ }),
540
+ );
541
+
542
+ const renderEffortView = () => h('div', { className: 'gp-dialog-body' },
543
+ error === null ? null : h(ui.ErrorText, null, error),
544
+ h(model.EffortListView, {
545
+ state: directoryState,
546
+ t,
547
+ disabled: controlBusy,
548
+ onPick: (effort) => {
549
+ if (directoryState.current === null || directoryState.current === undefined) return;
550
+ applySelection('effort', {
551
+ provider: directoryState.current.provider,
552
+ model: directoryState.current.model,
553
+ ...effort === undefined ? {} : { reasoningEffort: effort },
554
+ });
555
+ },
556
+ }),
557
+ );
558
+
559
+ const renderPresetView = () => h('div', { className: 'gp-dialog-body' },
560
+ h(preset.PresetOptions, {
561
+ state: presetState,
562
+ t,
563
+ selectedId: effectivePresetId,
564
+ onPick: (id) => {
565
+ setPresetChoice(id);
566
+ setView('clear');
567
+ },
568
+ onRetry: () => presetStore.load(true).catch(() => {}),
569
+ }),
570
+ );
571
+
572
+ const renderClearView = () => {
573
+ const preview = freshPreviewOf({
574
+ plan: review?.plan,
575
+ presetId: effectivePresetId,
576
+ instructions: instructionsState.texts,
577
+ workspaceDirectory,
578
+ runtimeFacts: previewFacts ?? undefined,
579
+ switches,
580
+ });
581
+ const instructionsOn = switches.includeOriginalInstructions === true;
582
+ const instructionsLoading = instructionsOn && instructionsState.status === 'loading';
583
+ const directoryLoading = directoryState.status === 'loading' || directoryState.status === 'idle';
584
+ const directoryError = directoryState.status === 'error';
585
+ const minimalDisabled = effectivePresetId !== 'minimal';
586
+ const runtimeIncluded = switches.includeMinimalContext === true && effectivePresetId === 'minimal';
587
+ const confirmDisabled = controlBusy || directoryLoading || instructionsLoading;
588
+ return h('div', { className: 'gp-dialog-body' },
589
+ planSection,
590
+ h(ui.WarningBox, null, t('clear.warning')),
591
+ h(ui.SelectRow, {
592
+ label: t('clear.preset'),
593
+ value: presetLabel,
594
+ disabled: controlBusy,
595
+ onClick: () => setView('preset'),
596
+ }),
597
+ h(ui.StaticRow, { label: t('clear.titleLabel'), value: previewTitle ?? t('clear.autoTitle') }),
598
+ h(ui.StaticRow, { label: t('clear.model'), value: model.summaryOf(directoryState, t) }),
599
+ h(ui.Note, null, t('clear.modelHint')),
600
+ h('div', { className: 'gp-plan' },
601
+ h('h3', { className: 'gp-plan-heading' }, t('preview.title')),
602
+ h('div', { className: 'gp-note' }, t('preview.blocks')),
603
+ (preview.blocks ?? []).map((block, index) => h('div', {
604
+ key: block?.id ?? `block-${index}`,
605
+ className: 'gp-static-row',
606
+ },
607
+ h('span', { className: 'gp-static-label' }, t(block?.titleKey ?? 'preview.title')),
608
+ h('span', { className: 'gp-static-value' }, block?.included === false
609
+ ? t('preview.excluded')
610
+ : `${t('preview.included')} · ${typeof block?.chars === 'number' ? block.chars : 0}`),
611
+ )),
612
+ instructionsOn
613
+ ? (
614
+ instructionsLoading
615
+ ? h(ui.Note, null, h(ui.Loading, null, t('preview.loadingInstructions')))
616
+ : instructionsState.status === 'ready'
617
+ ? h(ui.Note, null, t('preview.instructionCount', {
618
+ count: instructionsState.count,
619
+ chars: instructionsState.chars,
620
+ }))
621
+ : h(ui.Note, null, t('preview.instructionsUnavailable'))
622
+ )
623
+ : h(ui.Note, null, t('preview.noInstructions')),
624
+ h(ui.Note, null, t('preview.total', { chars: preview.totalChars ?? 0 })),
625
+ h(ui.Note, null, t('preview.approxTokens', { tokens: preview.approxTokens ?? 0 })),
626
+ preview.oversized === true
627
+ ? h(ui.WarningText, null, t('preview.oversized', { chars: preview.totalChars ?? 0 }))
628
+ : null,
629
+ runtimeIncluded ? h(ui.Note, null, t('preview.runtimeHint')) : null,
630
+ ),
631
+ h('div', { className: 'gp-checks', role: 'group', 'aria-label': t('preview.switches') },
632
+ h('div', { className: 'gp-check-heading' }, t('preview.switches')),
633
+ h(ui.Checkbox, {
634
+ label: t('preview.switchBudget'),
635
+ checked: switches.unlimitedBudget !== false,
636
+ disabled: controlBusy,
637
+ onChange: () => toggleSwitch('unlimitedBudget'),
638
+ }),
639
+ h(ui.Checkbox, {
640
+ label: t('preview.switchInstructions'),
641
+ checked: instructionsOn,
642
+ disabled: controlBusy,
643
+ onChange: () => toggleSwitch('includeOriginalInstructions'),
644
+ }),
645
+ h(ui.Checkbox, {
646
+ label: t('preview.switchMinimal'),
647
+ checked: switches.includeMinimalContext !== false,
648
+ disabled: controlBusy || minimalDisabled,
649
+ hint: minimalDisabled ? t('preview.minimalSwitchHint') : undefined,
650
+ onChange: () => toggleSwitch('includeMinimalContext'),
651
+ }),
652
+ ),
653
+ directoryLoading ? h(ui.Note, null, t('panel.modelDirectoryLoading')) : null,
654
+ directoryError ? h(ui.Note, null, t('panel.modelDirectoryError')) : null,
655
+ cleanupWarning === null ? null : h(ui.WarningText, null, cleanupWarning),
656
+ error === null ? null : h(ui.ErrorText, null, error),
657
+ h('div', { className: 'gp-clear-actions' },
658
+ h(ui.Button, {
659
+ variant: 'primary',
660
+ full: true,
661
+ disabled: confirmDisabled,
662
+ onClick: clearExecute,
663
+ }, busy === 'clear' ? h(ui.Loading, null, t('clear.busy')) : t('clear.confirm')),
664
+ ),
665
+ h(ui.Note, { className: 'gp-footer-note' }, t('clear.kept')),
666
+ );
667
+ };
668
+
669
+ const renderRootView = () => h('div', { className: 'gp-dialog-body' },
670
+ planSection,
671
+ h(model.ModelRows, {
672
+ state: directoryState,
673
+ t,
674
+ disabled: !modelAvailable || controlBusy,
675
+ onOpenModel: () => setView('model'),
676
+ onOpenEffort: () => setView('effort'),
677
+ onRetry: () => directory.load().catch(() => {}),
678
+ }),
679
+ !modelAvailable ? h(ui.Note, null, t('panel.subagentUnavailable')) : null,
680
+ renderCompactFailure(),
681
+ statusLine === null ? null : h('div', { className: 'gp-status', role: 'status' }, statusLine),
682
+ error === null ? null : h(ui.ErrorText, null, error),
683
+ compactFailure === null
684
+ ? h('div', null,
685
+ h('div', { className: 'gp-separator' }),
686
+ h('div', { className: 'gp-actions' },
687
+ h(ui.ActionRow, {
688
+ primary: true,
689
+ icon: h(ui.IconPlay),
690
+ title: t('panel.actions.execute.title'),
691
+ description: t('panel.actions.execute.description'),
692
+ disabled: controlBusy,
693
+ onClick: approvePending,
694
+ }),
695
+ h(ui.ActionRow, {
696
+ icon: h(ui.IconCompress),
697
+ title: t('panel.actions.compact.title'),
698
+ description: t('panel.actions.compact.description'),
699
+ disabled: controlBusy,
700
+ onClick: compact,
701
+ }),
702
+ h(ui.ActionRow, {
703
+ icon: h(ui.IconNewSession),
704
+ title: t('panel.actions.clear.title'),
705
+ description: t('panel.actions.clear.description'),
706
+ disabled: controlBusy || !modelAvailable,
707
+ onClick: enterClear,
708
+ }),
709
+ ),
710
+ )
711
+ : null,
712
+ canRevise
713
+ ? h('div', { className: 'gp-actions' },
714
+ h(ui.Button, {
715
+ variant: 'ghost',
716
+ full: true,
717
+ disabled: controlBusy,
718
+ onClick: revise,
719
+ }, busy === 'dismiss' ? h(ui.Loading, null, t('panel.loading')) : t('panel.revise')),
720
+ )
721
+ : null,
722
+ h(ui.Note, { className: 'gp-footer-note' }, t('panel.applyNote')),
723
+ );
724
+
725
+ const views = {
726
+ model: renderModelView,
727
+ effort: renderEffortView,
728
+ preset: renderPresetView,
729
+ clear: renderClearView,
730
+ root: renderRootView,
731
+ };
732
+ const body = views[view]();
733
+
734
+ return h(ui.Dialog, {
735
+ open,
736
+ onRequestClose: close,
737
+ blocked,
738
+ busy: busy !== null,
739
+ anchorRef: triggerRef,
740
+ labelledBy: 'gp-great-plan-title',
741
+ }, renderHeader(), body);
742
+ }
743
+
744
+ return {
745
+ ExecutionTrigger,
746
+ ExecutionDialog,
747
+ };
748
+ },
749
+ });