@bastani/atomic 0.8.20 → 0.8.21
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/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
- package/dist/builtin/subagents/agents/debugger.md +4 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +25 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
- package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
- package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
- package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +67 -3
- package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
- package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
- package/dist/builtin/workflows/src/shared/store.ts +37 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
- package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
- package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +2 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +11 -29
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/docs/quickstart.md +1 -2
- package/package.json +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
- package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
- package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
- package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
- package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
- package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
- package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
- package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
- package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
- package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
PendingPrompt,
|
|
8
8
|
PromptKind,
|
|
9
9
|
RunSnapshot,
|
|
10
|
+
StageInputRequest,
|
|
10
11
|
StageSnapshot,
|
|
11
12
|
StageNotice,
|
|
12
13
|
StoreSnapshot,
|
|
@@ -177,6 +178,16 @@ export interface Store {
|
|
|
177
178
|
* or restore it to running after the tool resolves.
|
|
178
179
|
*/
|
|
179
180
|
recordStageAwaitingInput(runId: string, stageId: string, awaiting: boolean, ts?: number): boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Record the serializable descriptor of a brokered structured prompt
|
|
183
|
+
* (`ask_user_question` / readiness gate) awaiting an answer on a stage.
|
|
184
|
+
* Surfaces the questions/options on the snapshot so `workflow send` and
|
|
185
|
+
* status inspection can answer the prompt headlessly. Resolution itself lives
|
|
186
|
+
* in `StageUiBroker`. Returns `true` when the descriptor changed.
|
|
187
|
+
*/
|
|
188
|
+
recordStageInputRequest(runId: string, stageId: string, request: StageInputRequest): boolean;
|
|
189
|
+
/** Clear a stage's brokered structured-prompt descriptor. Returns `true` when one was present. */
|
|
190
|
+
clearStageInputRequest(runId: string, stageId: string): boolean;
|
|
180
191
|
/**
|
|
181
192
|
* Mark a stage as `paused` and record `pausedAt`. Returns `true` when
|
|
182
193
|
* the stage transitioned (was not already paused, blocked, or terminal).
|
|
@@ -373,6 +384,7 @@ export function createStore(): Store {
|
|
|
373
384
|
if (stage.replayedFromStageId !== undefined) existing.replayedFromStageId = stage.replayedFromStageId;
|
|
374
385
|
if (stage.replayed !== undefined) existing.replayed = stage.replayed;
|
|
375
386
|
delete existing.awaitingInputSince;
|
|
387
|
+
delete existing.inputRequest;
|
|
376
388
|
rejectStagePrompt(existing, `pi-workflows: stage ${stage.id} ended before prompt resolved`);
|
|
377
389
|
_version++;
|
|
378
390
|
notify();
|
|
@@ -697,6 +709,31 @@ export function createStore(): Store {
|
|
|
697
709
|
return true;
|
|
698
710
|
},
|
|
699
711
|
|
|
712
|
+
recordStageInputRequest(runId: string, stageId: string, request: StageInputRequest): boolean {
|
|
713
|
+
const run = findRun(runId);
|
|
714
|
+
if (!run) return false;
|
|
715
|
+
if (TERMINAL_STATUSES.has(run.status)) return false;
|
|
716
|
+
const stage = findStage(run, stageId);
|
|
717
|
+
if (!stage) return false;
|
|
718
|
+
if (isTerminalStageStatus(stage.status)) return false;
|
|
719
|
+
if (stage.inputRequest?.id === request.id) return false;
|
|
720
|
+
stage.inputRequest = { ...request };
|
|
721
|
+
_version++;
|
|
722
|
+
notify();
|
|
723
|
+
return true;
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
clearStageInputRequest(runId: string, stageId: string): boolean {
|
|
727
|
+
const run = findRun(runId);
|
|
728
|
+
if (!run) return false;
|
|
729
|
+
const stage = findStage(run, stageId);
|
|
730
|
+
if (!stage || stage.inputRequest === undefined) return false;
|
|
731
|
+
delete stage.inputRequest;
|
|
732
|
+
_version++;
|
|
733
|
+
notify();
|
|
734
|
+
return true;
|
|
735
|
+
},
|
|
736
|
+
|
|
700
737
|
recordStageBlocked(runId: string, stageId: string, blockedBy: string): boolean {
|
|
701
738
|
const run = findRun(runId);
|
|
702
739
|
if (!run) return false;
|
|
@@ -60,7 +60,9 @@ export interface DispatchPayload {
|
|
|
60
60
|
/**
|
|
61
61
|
* Status list after `/workflow status`. The snapshot is captured (and
|
|
62
62
|
* `--all`-filtered) at emit time — scrollback entries don't live-update
|
|
63
|
-
* (the orchestrator widget owns live state).
|
|
63
|
+
* (the orchestrator widget owns live state). The wall-clock used for the
|
|
64
|
+
* `elapsed` / `running` labels is likewise frozen once per chat entry; see
|
|
65
|
+
* {@link makeComponent}.
|
|
64
66
|
*/
|
|
65
67
|
export interface StatusPayload {
|
|
66
68
|
kind: "status";
|
|
@@ -186,12 +188,27 @@ function makeComponent(
|
|
|
186
188
|
payload: ChatSurfacePayload,
|
|
187
189
|
theme: GraphTheme,
|
|
188
190
|
): CardComponent {
|
|
191
|
+
// Capture wall-clock ONCE, when the chat entry's component is created. The
|
|
192
|
+
// render() lambda below re-runs on every TUI frame: pi-tui's Container.render
|
|
193
|
+
// fans out to every child on each doRender, and the workflow/subagent live
|
|
194
|
+
// widgets call requestRender() ~12x/sec while runs are active. Without a
|
|
195
|
+
// frozen `now`, renderStatusList / renderRunDetail fall through to Date.now()
|
|
196
|
+
// on each frame, ticking the `elapsed` / `running` labels. Once the entry has
|
|
197
|
+
// scrolled above the viewport fold, that off-screen change pushes pi-tui's
|
|
198
|
+
// doRender() into the full-redraw branch (CSI 2J + CSI H + CSI 3J), which
|
|
199
|
+
// reads as a whole-screen flicker on terminals without synchronized-output
|
|
200
|
+
// support (notably mosh). Freezing here is also semantically right: these are
|
|
201
|
+
// point-in-time scrollback snapshots — the orchestrator widget owns live
|
|
202
|
+
// state. requestRender() never invalidates, so makeComponent runs once per
|
|
203
|
+
// entry; this mirrors the tool-result renderResult slot fix in
|
|
204
|
+
// src/extension/index.ts (capture `now` once, reuse it across renders).
|
|
205
|
+
const capturedNow = Date.now();
|
|
189
206
|
return {
|
|
190
207
|
render(width: number): string[] {
|
|
191
208
|
// pi passes the real chat content width; thread it down to every
|
|
192
209
|
// primitive so band fillers and card gaps land exactly on the
|
|
193
210
|
// right-edge cell. No `process.stdout.columns` heuristic needed.
|
|
194
|
-
return renderPayload(payload, theme, width).split("\n");
|
|
211
|
+
return renderPayload(payload, theme, width, capturedNow).split("\n");
|
|
195
212
|
},
|
|
196
213
|
invalidate() {
|
|
197
214
|
/* renders are pure of stored state; nothing to drop. */
|
|
@@ -203,6 +220,7 @@ function renderPayload(
|
|
|
203
220
|
payload: ChatSurfacePayload,
|
|
204
221
|
theme: GraphTheme,
|
|
205
222
|
width: number,
|
|
223
|
+
now: number,
|
|
206
224
|
): string {
|
|
207
225
|
switch (payload.kind) {
|
|
208
226
|
case "dispatch":
|
|
@@ -214,11 +232,11 @@ function renderPayload(
|
|
|
214
232
|
width,
|
|
215
233
|
});
|
|
216
234
|
case "status":
|
|
217
|
-
return renderStatusList(payload.runs, { theme, width });
|
|
235
|
+
return renderStatusList(payload.runs, { theme, width, now });
|
|
218
236
|
case "list":
|
|
219
237
|
return renderWorkflowList(payload.entries, { theme, width });
|
|
220
238
|
case "detail":
|
|
221
|
-
return renderRunDetail(payload.detail, { theme, width });
|
|
239
|
+
return renderRunDetail(payload.detail, { theme, width, now });
|
|
222
240
|
case "killed":
|
|
223
241
|
return renderWorkflowKilledNotice({
|
|
224
242
|
width,
|
|
@@ -191,6 +191,7 @@ export class GraphView implements Component {
|
|
|
191
191
|
private graphScrollOffset = 0;
|
|
192
192
|
private graphScrollColOffset = 0;
|
|
193
193
|
private pendingEnsureFocusedVisible = true;
|
|
194
|
+
private lastAutoFocusedAwaitingInputKey: string | null = null;
|
|
194
195
|
|
|
195
196
|
private _intervalId: ReturnType<typeof setInterval> | null = null;
|
|
196
197
|
private _lastGTime: number | null = null;
|
|
@@ -278,6 +279,17 @@ export class GraphView implements Component {
|
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
const awaitingTarget = this._awaitingInputFocusTarget();
|
|
283
|
+
if (awaitingTarget) {
|
|
284
|
+
if (awaitingTarget.key !== this.lastAutoFocusedAwaitingInputKey) {
|
|
285
|
+
this.focusedIndex = awaitingTarget.index;
|
|
286
|
+
focusNeedsReveal = true;
|
|
287
|
+
this.lastAutoFocusedAwaitingInputKey = awaitingTarget.key;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
this.lastAutoFocusedAwaitingInputKey = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
if (this.cachedLayout.length === 0) {
|
|
282
294
|
this.focusedIndex = 0;
|
|
283
295
|
this.graphScrollOffset = 0;
|
|
@@ -290,6 +302,41 @@ export class GraphView implements Component {
|
|
|
290
302
|
this._syncPromptState(run.pendingPrompt);
|
|
291
303
|
}
|
|
292
304
|
|
|
305
|
+
private _awaitingInputFocusTarget(): { index: number; key: string } | null {
|
|
306
|
+
let newest: { index: number; key: string; createdAt: number } | null = null;
|
|
307
|
+
for (let index = 0; index < this.cachedLayout.length; index++) {
|
|
308
|
+
const stage = this.cachedLayout[index]!.stage;
|
|
309
|
+
const target = this._awaitingInputKey(stage);
|
|
310
|
+
if (!target) continue;
|
|
311
|
+
if (!newest || target.createdAt >= newest.createdAt) {
|
|
312
|
+
newest = { index, key: target.key, createdAt: target.createdAt };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return newest ? { index: newest.index, key: newest.key } : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private _awaitingInputKey(stage: StageSnapshot): { key: string; createdAt: number } | null {
|
|
319
|
+
if (stage.pendingPrompt) {
|
|
320
|
+
return {
|
|
321
|
+
key: `prompt:${stage.id}:${stage.pendingPrompt.id}`,
|
|
322
|
+
createdAt: stage.pendingPrompt.createdAt,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (stage.inputRequest) {
|
|
326
|
+
return {
|
|
327
|
+
key: `input-request:${stage.id}:${stage.inputRequest.id}`,
|
|
328
|
+
createdAt: stage.inputRequest.createdAt,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (stage.status === "awaiting_input") {
|
|
332
|
+
return {
|
|
333
|
+
key: `awaiting:${stage.id}:${stage.awaitingInputSince ?? "active"}`,
|
|
334
|
+
createdAt: stage.awaitingInputSince ?? stage.startedAt ?? 0,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
293
340
|
private _graphStages(run: RunSnapshot): StageSnapshot[] {
|
|
294
341
|
const hasStagePrompt = run.stages.some((stage) => stage.pendingPrompt !== undefined);
|
|
295
342
|
if (!hasStagePrompt) return [...run.stages];
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { Store } from "../shared/store.js";
|
|
19
|
+
import type { StoreSnapshot } from "../shared/store-types.js";
|
|
19
20
|
import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bastani/atomic";
|
|
20
21
|
import { WorkflowAttachPane } from "./workflow-attach-pane.js";
|
|
21
22
|
import { deriveGraphThemeFromPiTheme } from "./graph-theme.js";
|
|
@@ -176,12 +177,31 @@ export function buildGraphOverlayAdapter(
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function snapshotHasAwaitingInput(snapshot: StoreSnapshot): boolean {
|
|
181
|
+
return snapshot.runs.some(
|
|
182
|
+
(run) => run.pendingPrompt !== undefined || run.stages.some(
|
|
183
|
+
(stage) => stage.status === "awaiting_input"
|
|
184
|
+
|| stage.pendingPrompt !== undefined
|
|
185
|
+
|| stage.inputRequest !== undefined,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function refocusVisibleOverlayForAwaitingInput(snapshot: StoreSnapshot): void {
|
|
191
|
+
if (!snapshotHasAwaitingInput(snapshot)) return;
|
|
192
|
+
if (currentHandle === null) return;
|
|
193
|
+
if (currentHandle.isHidden()) return;
|
|
194
|
+
if (currentHandle.isFocused()) return;
|
|
195
|
+
currentHandle.focus();
|
|
196
|
+
}
|
|
197
|
+
|
|
179
198
|
function makeComponent(
|
|
180
199
|
view: WorkflowAttachPane,
|
|
181
200
|
tui: PiCustomOverlayFactoryTui,
|
|
182
201
|
): PiCustomComponent {
|
|
183
|
-
const onStoreUpdate = (): void => {
|
|
202
|
+
const onStoreUpdate = (snapshot: StoreSnapshot): void => {
|
|
184
203
|
view.invalidate();
|
|
204
|
+
refocusVisibleOverlayForAwaitingInput(snapshot);
|
|
185
205
|
tui.requestRender?.();
|
|
186
206
|
};
|
|
187
207
|
const unsubscribe = store.subscribe(onStoreUpdate);
|
|
@@ -217,6 +237,13 @@ export function buildGraphOverlayAdapter(
|
|
|
217
237
|
if (mounted) {
|
|
218
238
|
currentView?.retarget(runId, stageId);
|
|
219
239
|
setMouseScrollTracking(currentView?.wantsMouseScrollTracking() ?? true);
|
|
240
|
+
// Restore keyboard focus to the visible overlay after retargeting.
|
|
241
|
+
// pi-tui dispatches key events only to the focused component, so a
|
|
242
|
+
// mounted-but-visible overlay that is retargeted (e.g. to a stage-scoped
|
|
243
|
+
// HIL prompt / readiness gate) would otherwise appear frozen — arrows,
|
|
244
|
+
// Enter, Ctrl+D and `q` all dead — if focus stayed on an underlying or
|
|
245
|
+
// previously-focused pane (issue #1120).
|
|
246
|
+
currentHandle?.focus();
|
|
220
247
|
return;
|
|
221
248
|
}
|
|
222
249
|
|
|
@@ -275,6 +302,21 @@ export function buildGraphOverlayAdapter(
|
|
|
275
302
|
if (currentHandle?.isHidden() === true) return;
|
|
276
303
|
tui.requestRender?.();
|
|
277
304
|
},
|
|
305
|
+
// Re-assert overlay keyboard focus on demand. The attached stage chat
|
|
306
|
+
// calls this when it shows a broker custom UI (e.g. the readiness gate)
|
|
307
|
+
// so the gate receives input even if focus drifted off the overlay
|
|
308
|
+
// while the agent's turn was streaming (#1120).
|
|
309
|
+
requestFocus: () => {
|
|
310
|
+
if (currentHandle?.isHidden() === true) return;
|
|
311
|
+
// Idempotent: only grab focus if the overlay does not already own it.
|
|
312
|
+
// A redundant focus() while already focused re-runs pi-tui's focus
|
|
313
|
+
// transition mid-stream and stalls the agent's continuation (#1120,
|
|
314
|
+
// the "ac" freeze). Skipping the no-op case lets callers ask for focus
|
|
315
|
+
// freely — e.g. when showing a mid-turn ask_user_question — without a
|
|
316
|
+
// fragile "only when not streaming" guard at every call site.
|
|
317
|
+
if (currentHandle?.isFocused() === true) return;
|
|
318
|
+
currentHandle?.focus();
|
|
319
|
+
},
|
|
278
320
|
setMouseScrollTracking,
|
|
279
321
|
} as ConstructorParameters<typeof WorkflowAttachPane>[0] & {
|
|
280
322
|
piTui?: PiCustomOverlayFactoryTui;
|
|
@@ -202,7 +202,7 @@ function artifactRowsFor(detail: RunDetail): Array<[string, string]> {
|
|
|
202
202
|
function stageLinePlain(stage: StageSnapshot, now: number, width: number): string {
|
|
203
203
|
const icon = statusIcon(stage.status);
|
|
204
204
|
const dur = stageDurationString(stage, now);
|
|
205
|
-
const activity = stageActivityString(stage);
|
|
205
|
+
const activity = stageActivityString(stage, now);
|
|
206
206
|
const name = truncateToWidth(`${icon} ${stage.name}`, STAGE_NAME_COL + 2, "…");
|
|
207
207
|
const activityText = activity ? truncateToWidth(activity, 16, "…") : undefined;
|
|
208
208
|
const parts = [
|
|
@@ -222,7 +222,7 @@ function stageLineThemed(stage: StageSnapshot, now: number, theme: GraphTheme, w
|
|
|
222
222
|
const dim = hexToAnsi(theme.dim);
|
|
223
223
|
const stateFg = hexToAnsi(statusColor(stage.status, theme));
|
|
224
224
|
|
|
225
|
-
const activity = stageActivityString(stage);
|
|
225
|
+
const activity = stageActivityString(stage, now);
|
|
226
226
|
const dur = stageDurationString(stage, now);
|
|
227
227
|
|
|
228
228
|
const nameText = truncateToWidth(stage.name, STAGE_NAME_COL, "…");
|
|
@@ -268,7 +268,13 @@ function stageDurationString(stage: StageSnapshot, now: number): string | undefi
|
|
|
268
268
|
return elapsed === undefined ? undefined : fmtDuration(elapsed);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
// `now` is the stable, capture-once clock threaded down from renderRunDetail
|
|
272
|
+
// (opts.now). Using it — not a fresh Date.now() — keeps a running stage's active
|
|
273
|
+
// tool-activity label (e.g. `bash · 6s`) byte-stable across host re-renders so a
|
|
274
|
+
// scrollback run-detail card that has scrolled above the viewport fold does not
|
|
275
|
+
// retrigger pi-tui's full-screen redraw (CSI 2J/H/3J) every render tick. The
|
|
276
|
+
// companion below-editor widget owns the live, ticking view.
|
|
277
|
+
function stageActivityString(stage: StageSnapshot, now: number): string | undefined {
|
|
272
278
|
if (stage.status !== "running") return undefined;
|
|
273
279
|
const last = stage.toolEvents.at(-1);
|
|
274
280
|
if (!last) return undefined;
|
|
@@ -276,7 +282,7 @@ function stageActivityString(stage: StageSnapshot): string | undefined {
|
|
|
276
282
|
return `${last.name} · ${fmtDuration(last.endedAt - last.startedAt)}`;
|
|
277
283
|
}
|
|
278
284
|
if (last.startedAt !== undefined) {
|
|
279
|
-
return `${last.name} · ${fmtDuration(
|
|
285
|
+
return `${last.name} · ${fmtDuration(now - last.startedAt)}`;
|
|
280
286
|
}
|
|
281
287
|
return last.name;
|
|
282
288
|
}
|
|
@@ -109,6 +109,12 @@ export interface StageChatViewOpts {
|
|
|
109
109
|
onClose: () => void;
|
|
110
110
|
/** Request a host TUI repaint after SDK events mutate local chat state. */
|
|
111
111
|
requestRender?: () => void;
|
|
112
|
+
/**
|
|
113
|
+
* Re-assert overlay keyboard focus. Showing a stage custom UI (e.g. the
|
|
114
|
+
* readiness gate) must make the overlay the focused pi-tui component again,
|
|
115
|
+
* otherwise key events keep going elsewhere and the UI looks frozen (#1120).
|
|
116
|
+
*/
|
|
117
|
+
requestFocus?: () => void;
|
|
112
118
|
/** Live pi-tui host objects. When present, stage input uses pi's editor UI. */
|
|
113
119
|
piTui?: TUI;
|
|
114
120
|
piTheme?: unknown;
|
|
@@ -190,6 +196,8 @@ export class StageChatView implements Component, Focusable {
|
|
|
190
196
|
private onDetach: () => void;
|
|
191
197
|
private onClose: () => void;
|
|
192
198
|
private requestRender: (() => void) | undefined;
|
|
199
|
+
private requestFocus: (() => void) | undefined;
|
|
200
|
+
private focusHoldTimer: ReturnType<typeof setInterval> | undefined;
|
|
193
201
|
private getViewportRows?: () => number | undefined;
|
|
194
202
|
private piTui?: TUI;
|
|
195
203
|
private piTheme?: unknown;
|
|
@@ -198,6 +206,7 @@ export class StageChatView implements Component, Focusable {
|
|
|
198
206
|
private chatHost: ChatSessionHost<NoticeEntry>;
|
|
199
207
|
private stageUiBroker: StageUiBroker;
|
|
200
208
|
private mountedCustomUi: MountedStageCustomUi | null = null;
|
|
209
|
+
private mountingRequestId: string | null = null;
|
|
201
210
|
private promptState: PromptCardState | null = null;
|
|
202
211
|
private promptEditor: EditorComponent | null = null;
|
|
203
212
|
private promptEditorPromptId: string | null = null;
|
|
@@ -227,6 +236,25 @@ export class StageChatView implements Component, Focusable {
|
|
|
227
236
|
this.onDetach = opts.onDetach;
|
|
228
237
|
this.onClose = opts.onClose;
|
|
229
238
|
this.requestRender = opts.requestRender;
|
|
239
|
+
this.requestFocus = opts.requestFocus;
|
|
240
|
+
// Hold overlay keyboard focus against host focus-steals. pi-tui overlays
|
|
241
|
+
// capture focus on show, but any tui.setFocus() elsewhere (the host editor
|
|
242
|
+
// during background workflow activity) steals it, leaving the gate/composer
|
|
243
|
+
// input-dead. Re-claim it on a short interval — only while NOT streaming, so
|
|
244
|
+
// a mid-turn ask_user_question never refocuses during the agent's
|
|
245
|
+
// continuation (which would stall the stream).
|
|
246
|
+
if (opts.requestFocus) {
|
|
247
|
+
this.focusHoldTimer = setInterval(() => {
|
|
248
|
+
// Hold focus on the overlay whenever there is something to interact
|
|
249
|
+
// with: a mounted custom UI (ask_user_question / readiness gate) must
|
|
250
|
+
// stay answerable even mid-turn, and an idle composer should keep focus.
|
|
251
|
+
// During a pure streaming continuation (no custom UI mounted) we leave
|
|
252
|
+
// focus alone so we never reclaim it out from under the agent's live
|
|
253
|
+
// output. requestFocus is idempotent, so this is a no-op whenever the
|
|
254
|
+
// overlay already owns focus.
|
|
255
|
+
if (this.mountedCustomUi !== null || !this._isStreaming()) this.requestFocus?.();
|
|
256
|
+
}, 150);
|
|
257
|
+
}
|
|
230
258
|
this.getViewportRows = opts.getViewportRows;
|
|
231
259
|
this.piTui = opts.piTui;
|
|
232
260
|
this.piTheme = opts.piTheme;
|
|
@@ -367,7 +395,15 @@ export class StageChatView implements Component, Focusable {
|
|
|
367
395
|
private async _showCustomUi(request: StageCustomUiRequest): Promise<void> {
|
|
368
396
|
this.mountedCustomUi?.component.dispose?.();
|
|
369
397
|
this.mountedCustomUi = null;
|
|
398
|
+
// Track the request currently being mounted. `mountStageCustomUi` is async,
|
|
399
|
+
// so the broker can resolve/reject/abort the request (clearing it via
|
|
400
|
+
// `_hideMountedCustomUi`) before we finish awaiting. Without this guard the
|
|
401
|
+
// post-await assignment below would strand a settled gate as a permanent
|
|
402
|
+
// `mountedCustomUi`, hiding the transcript and crashing on the next
|
|
403
|
+
// keystroke routed into the dead component (readiness gate #1099).
|
|
404
|
+
this.mountingRequestId = request.id;
|
|
370
405
|
if (!this.piTui || this.piTheme === undefined || this.piKeybindings === undefined) {
|
|
406
|
+
this.mountingRequestId = null;
|
|
371
407
|
this.stageUiBroker.reject(
|
|
372
408
|
request,
|
|
373
409
|
new Error("pi-workflows: stage custom UI cannot mount without attached TUI host"),
|
|
@@ -375,7 +411,7 @@ export class StageChatView implements Component, Focusable {
|
|
|
375
411
|
return;
|
|
376
412
|
}
|
|
377
413
|
try {
|
|
378
|
-
|
|
414
|
+
const mounted = await mountStageCustomUi(
|
|
379
415
|
request,
|
|
380
416
|
this.piTui,
|
|
381
417
|
this.piTheme,
|
|
@@ -390,8 +426,25 @@ export class StageChatView implements Component, Focusable {
|
|
|
390
426
|
this.requestRender?.();
|
|
391
427
|
},
|
|
392
428
|
);
|
|
429
|
+
// Settled or superseded while mounting: drop the freshly-built component
|
|
430
|
+
// instead of showing a gate the broker has already torn down.
|
|
431
|
+
if (this.mountingRequestId !== request.id) {
|
|
432
|
+
mounted.component.dispose?.();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.mountingRequestId = null;
|
|
436
|
+
this.mountedCustomUi = mounted;
|
|
437
|
+
// A freshly-shown custom UI (ask_user_question / readiness gate) must own
|
|
438
|
+
// keyboard focus to be answerable — including a question mounted mid-turn
|
|
439
|
+
// while the agent is "streaming" (it is blocked on this very question, and
|
|
440
|
+
// host focus may have drifted off the overlay during the turn, e.g. after a
|
|
441
|
+
// stay-loop composer submit). requestFocus is idempotent (a no-op when the
|
|
442
|
+
// overlay already owns focus), so this never re-runs a redundant focus
|
|
443
|
+
// transition that would stall the stream (#1120).
|
|
444
|
+
this.requestFocus?.();
|
|
393
445
|
this.requestRender?.();
|
|
394
446
|
} catch (error) {
|
|
447
|
+
if (this.mountingRequestId === request.id) this.mountingRequestId = null;
|
|
395
448
|
this.stageUiBroker.reject(request, error);
|
|
396
449
|
}
|
|
397
450
|
}
|
|
@@ -579,6 +632,13 @@ export class StageChatView implements Component, Focusable {
|
|
|
579
632
|
this._syncPromptState(stage?.pendingPrompt);
|
|
580
633
|
const promptActive = !customUiActive && this.promptState !== null;
|
|
581
634
|
const readOnlyArchive = this._isReadOnlyArchive(stage);
|
|
635
|
+
|
|
636
|
+
// ask_user_question / readiness-gate custom UI renders as a bottom panel
|
|
637
|
+
// (in the high-priority composer slot) so the live transcript stays visible
|
|
638
|
+
// and scrollable above it — matching the standalone ask_user_question tool.
|
|
639
|
+
// Structured prompt nodes and read-only archives keep their full-body
|
|
640
|
+
// treatment below.
|
|
641
|
+
const customUiLines = customUiActive ? this._renderCustomUi(w) : [];
|
|
582
642
|
const chatChromeHidden = customUiActive || promptActive || readOnlyArchive;
|
|
583
643
|
const pendingLines = chatChromeHidden ? [] : this.chatHost.renderPendingMessages(w);
|
|
584
644
|
const workingLines = chatChromeHidden ? [] : this.chatHost.renderWorkingStatus(w);
|
|
@@ -594,27 +654,32 @@ export class StageChatView implements Component, Focusable {
|
|
|
594
654
|
pendingRows: pendingLines.length,
|
|
595
655
|
workingRows: workingLines.length,
|
|
596
656
|
usageRows: usageLines.length,
|
|
597
|
-
|
|
657
|
+
// The custom UI question takes the reserved bottom (composer) slot so the
|
|
658
|
+
// transcript above keeps as much room as possible and the question never
|
|
659
|
+
// clips below the overlay boundary.
|
|
660
|
+
editorRows: customUiActive ? customUiLines.length : editorLines.length,
|
|
598
661
|
footerRows: footerLines.length,
|
|
599
662
|
});
|
|
600
663
|
const visiblePendingLines = pendingLines.slice(0, plan.pendingRows);
|
|
601
664
|
const visibleWorkingLines = workingLines.slice(0, plan.workingRows);
|
|
602
665
|
const visibleUsageLines = usageLines.slice(0, plan.usageRows);
|
|
603
|
-
const visibleEditorLines =
|
|
666
|
+
const visibleEditorLines = customUiActive
|
|
667
|
+
? customUiLines.slice(0, plan.editorRows)
|
|
668
|
+
: editorLines.slice(0, plan.editorRows);
|
|
604
669
|
const visibleFooterLines = footerLines.slice(0, plan.footerRows);
|
|
605
670
|
const bodyBudget = plan.bodyRows;
|
|
606
671
|
if (blocked) this.chatHost.scrollToBottom();
|
|
607
672
|
|
|
608
673
|
let bodyLines: string[];
|
|
609
|
-
if (
|
|
610
|
-
bodyLines = this._renderCustomUiBody(w, bodyBudget);
|
|
611
|
-
} else if (promptActive) {
|
|
674
|
+
if (promptActive) {
|
|
612
675
|
bodyLines = this._renderPromptBody(w, bodyBudget);
|
|
613
676
|
} else if (blocked) {
|
|
614
677
|
bodyLines = this._renderBlockedBody(w, bodyBudget, stage);
|
|
615
678
|
} else if (readOnlyArchive) {
|
|
616
679
|
bodyLines = this._renderReadOnlyArchiveBody(w, bodyBudget, stage);
|
|
617
680
|
} else {
|
|
681
|
+
// Live transcript. When a custom UI question is active it renders in the
|
|
682
|
+
// composer slot above; the transcript here stays visible and scrollable.
|
|
618
683
|
bodyLines = this.chatHost.renderBody(w, bodyBudget);
|
|
619
684
|
}
|
|
620
685
|
|
|
@@ -844,11 +909,13 @@ export class StageChatView implements Component, Focusable {
|
|
|
844
909
|
return lines;
|
|
845
910
|
}
|
|
846
911
|
|
|
847
|
-
|
|
912
|
+
// Natural-height render of the mounted custom UI (no body padding): it is
|
|
913
|
+
// placed in the composer slot so the transcript stays scrollable above it.
|
|
914
|
+
private _renderCustomUi(width: number): string[] {
|
|
848
915
|
const component = this.mountedCustomUi?.component;
|
|
849
|
-
if (component)
|
|
850
|
-
|
|
851
|
-
return
|
|
916
|
+
if (!component) return [];
|
|
917
|
+
setComponentFocused(component, this.focused);
|
|
918
|
+
return component.render(width);
|
|
852
919
|
}
|
|
853
920
|
|
|
854
921
|
private _renderPromptBody(width: number, budget: number): string[] {
|
|
@@ -1041,16 +1108,30 @@ export class StageChatView implements Component, Focusable {
|
|
|
1041
1108
|
handleInput(data: string): boolean {
|
|
1042
1109
|
if (this.mountedCustomUi) {
|
|
1043
1110
|
if (matchesKey(data, Key.ctrl("d"))) {
|
|
1044
|
-
|
|
1111
|
+
// Detach stops *viewing* the stage; it does not cancel a pending
|
|
1112
|
+
// human-input request. Release the local display only — the request
|
|
1113
|
+
// stays pending (the stage remains awaiting_input) and is re-displayed
|
|
1114
|
+
// when the user re-attaches.
|
|
1115
|
+
this._releaseMountedCustomUi();
|
|
1045
1116
|
if (this._isPaused()) this.onClose();
|
|
1046
1117
|
else this.onDetach();
|
|
1047
1118
|
return true;
|
|
1048
1119
|
}
|
|
1049
1120
|
if (matchesKey(data, Key.ctrl("c"))) {
|
|
1050
|
-
|
|
1121
|
+
// Close hides the overlay; the background run — and its pending
|
|
1122
|
+
// human-input request — keep living. Release the local display only.
|
|
1123
|
+
this._releaseMountedCustomUi();
|
|
1051
1124
|
this.onClose();
|
|
1052
1125
|
return true;
|
|
1053
1126
|
}
|
|
1127
|
+
// Let scroll input (mouse wheel / pageUp / pageDown / home / end) reach
|
|
1128
|
+
// the transcript so history stays scrollable while the question is shown,
|
|
1129
|
+
// matching the standalone ask_user_question tool. Navigation keys
|
|
1130
|
+
// (arrows / enter / typing) fall through to the question component.
|
|
1131
|
+
if (this.chatHost.handleScrollInput(data)) {
|
|
1132
|
+
this.requestRender?.();
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1054
1135
|
setComponentFocused(this.mountedCustomUi.component, this.focused);
|
|
1055
1136
|
this.mountedCustomUi.component.handleInput?.(data);
|
|
1056
1137
|
this.requestRender?.();
|
|
@@ -1165,11 +1246,15 @@ export class StageChatView implements Component, Focusable {
|
|
|
1165
1246
|
}
|
|
1166
1247
|
|
|
1167
1248
|
dispose(): void {
|
|
1249
|
+
if (this.focusHoldTimer !== undefined) {
|
|
1250
|
+
clearInterval(this.focusHoldTimer);
|
|
1251
|
+
this.focusHoldTimer = undefined;
|
|
1252
|
+
}
|
|
1168
1253
|
this._unsubscribeStore?.();
|
|
1169
1254
|
this._unsubscribeStore = null;
|
|
1170
1255
|
this._unsubscribeHandle?.();
|
|
1171
1256
|
this._unsubscribeHandle = null;
|
|
1172
|
-
this.
|
|
1257
|
+
this._releaseMountedCustomUi();
|
|
1173
1258
|
this._disposePromptEditor();
|
|
1174
1259
|
this._unregisterStageUiHost?.();
|
|
1175
1260
|
this._unregisterStageUiHost = null;
|
|
@@ -1177,20 +1262,37 @@ export class StageChatView implements Component, Focusable {
|
|
|
1177
1262
|
}
|
|
1178
1263
|
|
|
1179
1264
|
private _hideMountedCustomUi(request: StageCustomUiRequest): void {
|
|
1265
|
+
// Signal any in-flight `_showCustomUi` mount for this request to drop its
|
|
1266
|
+
// component when it finishes — the broker is already tearing it down.
|
|
1267
|
+
if (this.mountingRequestId === request.id) this.mountingRequestId = null;
|
|
1180
1268
|
const mounted = this.mountedCustomUi;
|
|
1181
1269
|
if (!mounted || mounted.request.id !== request.id) return;
|
|
1182
1270
|
this.mountedCustomUi = null;
|
|
1183
1271
|
mounted.component.dispose?.();
|
|
1184
1272
|
this.chatHost.focused = this.focused;
|
|
1185
1273
|
this.chatHost.scrollToBottom();
|
|
1274
|
+
// Returning to the composer after a custom UI resolves (e.g. the readiness
|
|
1275
|
+
// gate -> "stay") must re-assert overlay focus so the composer accepts
|
|
1276
|
+
// input. Guarded for streaming so an answered mid-turn ask_user_question
|
|
1277
|
+
// does not refocus during the agent's continuation (would stall it).
|
|
1278
|
+
if (!this._isStreaming()) this.requestFocus?.();
|
|
1186
1279
|
this.requestRender?.();
|
|
1187
1280
|
}
|
|
1188
1281
|
|
|
1189
|
-
|
|
1282
|
+
/**
|
|
1283
|
+
* Stop displaying the mounted stage custom UI locally, WITHOUT settling its
|
|
1284
|
+
* broker request. Detaching / closing / disposing the attached chat stops
|
|
1285
|
+
* viewing the stage; it never cancels a pending human-input request. The
|
|
1286
|
+
* request stays pending (the stage remains awaiting_input) so re-attaching
|
|
1287
|
+
* re-displays it. The request is settled only by the user answering (broker
|
|
1288
|
+
* resolve) or the run aborting (its AbortSignal -> broker reject) — those are
|
|
1289
|
+
* the single chokepoints for ending a human-input request.
|
|
1290
|
+
*/
|
|
1291
|
+
private _releaseMountedCustomUi(): void {
|
|
1292
|
+
this.mountingRequestId = null;
|
|
1190
1293
|
const mounted = this.mountedCustomUi;
|
|
1191
1294
|
if (!mounted) return;
|
|
1192
1295
|
this.mountedCustomUi = null;
|
|
1193
|
-
this.stageUiBroker.reject(mounted.request, new Error(`pi-workflows: ${message}`));
|
|
1194
1296
|
mounted.component.dispose?.();
|
|
1195
1297
|
}
|
|
1196
1298
|
|
|
@@ -100,6 +100,12 @@ export interface WorkflowAttachPaneOpts {
|
|
|
100
100
|
* visibility so a hidden pane stays cheap.
|
|
101
101
|
*/
|
|
102
102
|
requestRender?: () => void;
|
|
103
|
+
/**
|
|
104
|
+
* Host hook to re-assert overlay keyboard focus. Threaded into the attached
|
|
105
|
+
* stage chat so showing a broker custom UI (e.g. the readiness gate) refocuses
|
|
106
|
+
* the overlay and the UI is not left input-dead (#1120).
|
|
107
|
+
*/
|
|
108
|
+
requestFocus?: () => void;
|
|
103
109
|
/**
|
|
104
110
|
* Host hook for terminal mouse reporting. Graph mode uses wheel input
|
|
105
111
|
* for canvas scrolling; stage-chat mode uses it for transcript history
|
|
@@ -125,6 +131,7 @@ export class WorkflowAttachPane implements Component {
|
|
|
125
131
|
private onPromptResolve?: (runId: string, promptId: string, response: unknown) => void;
|
|
126
132
|
private getViewportRows?: () => number | undefined;
|
|
127
133
|
private hostRequestRender?: () => void;
|
|
134
|
+
private hostRequestFocus?: () => void;
|
|
128
135
|
private setMouseScrollTracking?: (enabled: boolean) => void;
|
|
129
136
|
private piTui?: TUI;
|
|
130
137
|
private piTheme?: unknown;
|
|
@@ -152,6 +159,7 @@ export class WorkflowAttachPane implements Component {
|
|
|
152
159
|
this.onPromptResolve = opts.onPromptResolve;
|
|
153
160
|
this.getViewportRows = opts.getViewportRows;
|
|
154
161
|
this.hostRequestRender = opts.requestRender;
|
|
162
|
+
this.hostRequestFocus = opts.requestFocus;
|
|
155
163
|
this.setMouseScrollTracking = opts.setMouseScrollTracking;
|
|
156
164
|
this.piTui = opts.piTui;
|
|
157
165
|
this.piTheme = opts.piTheme;
|
|
@@ -233,6 +241,7 @@ export class WorkflowAttachPane implements Component {
|
|
|
233
241
|
onDetach: () => this._detachFromStage(),
|
|
234
242
|
onClose: this.onClose,
|
|
235
243
|
requestRender: this.hostRequestRender,
|
|
244
|
+
requestFocus: this.hostRequestFocus,
|
|
236
245
|
piTui: this.piTui,
|
|
237
246
|
piTheme: this.piTheme,
|
|
238
247
|
piKeybindings: this.piKeybindings,
|