@bastani/atomic 0.8.21-0 → 0.8.22-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/CHANGELOG.md +46 -9
- package/dist/builtin/intercom/broker/broker.ts +3 -3
- package/dist/builtin/intercom/config.ts +3 -3
- package/dist/builtin/intercom/index.ts +1 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/ui/compose.ts +2 -2
- package/dist/builtin/mcp/host-html-template.ts +0 -3
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +13 -4
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
- package/dist/builtin/subagents/agents/debugger.md +6 -6
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
- package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
- package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
- package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
- package/dist/builtin/subagents/src/agents/skills.ts +19 -1
- package/dist/builtin/subagents/src/extension/index.ts +24 -22
- package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
- package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
- package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
- package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
- package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
- package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
- package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
- package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
- package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
- package/dist/builtin/subagents/src/shared/types.ts +4 -2
- package/dist/builtin/subagents/src/shared/utils.ts +3 -61
- package/dist/builtin/subagents/src/tui/render.ts +303 -157
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +101 -35
- package/dist/builtin/workflows/README.md +228 -41
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
- package/dist/builtin/workflows/builtin/goal.ts +39 -25
- package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
- package/dist/builtin/workflows/builtin/ralph.ts +21 -21
- package/dist/builtin/workflows/package.json +6 -5
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
- package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
- package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
- package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
- package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
- package/dist/builtin/workflows/src/extension/index.ts +690 -352
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
- package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
- package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
- package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
- package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
- package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
- package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
- package/dist/builtin/workflows/src/index.ts +2 -0
- package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
- package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
- package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
- package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
- package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
- package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
- package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
- package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
- package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
- package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
- package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
- package/dist/builtin/workflows/src/shared/store.ts +58 -14
- package/dist/builtin/workflows/src/shared/types.ts +105 -40
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
- package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
- package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
- package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
- package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
- package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
- package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
- package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
- package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
- package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
- package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
- package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
- package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
- package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
- package/dist/builtin/workflows/src/tui/toast.ts +2 -2
- package/dist/builtin/workflows/src/tui/widget.ts +53 -12
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
- package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
- package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +45 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +27 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +196 -17
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -2
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/codex-fast-mode.d.ts +36 -0
- package/dist/core/codex-fast-mode.d.ts.map +1 -0
- package/dist/core/codex-fast-mode.js +117 -0
- package/dist/core/codex-fast-mode.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/index.d.ts +4 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +7 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +23 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/reactive-widget.d.ts +58 -0
- package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
- package/dist/core/extensions/reactive-widget.js +182 -0
- package/dist/core/extensions/reactive-widget.js.map +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +26 -12
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +8 -2
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +11 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +49 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +22 -13
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +7 -5
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +5 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +16 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +64 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +7 -4
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
- package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
- package/dist/modes/interactive/chat-input-actions.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +0 -1
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
- package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
- package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -12
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +132 -30
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +53 -6
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +2 -2
- package/docs/development.md +2 -2
- package/docs/docs.json +2 -2
- package/docs/extensions.md +18 -13
- package/docs/providers.md +5 -1
- package/docs/quickstart.md +5 -3
- package/docs/rpc.md +5 -5
- package/docs/sdk.md +12 -12
- package/docs/settings.md +18 -0
- package/docs/themes.md +6 -6
- package/docs/tui.md +20 -18
- package/docs/usage.md +2 -0
- package/docs/workflows.md +403 -39
- package/examples/extensions/qna.ts +2 -2
- package/package.json +4 -4
- package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
- package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
- package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
- package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
- package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
- package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
- package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
- package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
- package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
- package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
- package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
|
@@ -25,12 +25,19 @@ import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bast
|
|
|
25
25
|
import type { Store } from "../shared/store.js";
|
|
26
26
|
import type { GraphTheme } from "./graph-theme.js";
|
|
27
27
|
import { GraphView } from "./graph-view.js";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
StageChatView,
|
|
30
|
+
type StageChatDetachMetadata,
|
|
31
|
+
type StageChatDetachReason,
|
|
32
|
+
} from "./stage-chat-view.js";
|
|
33
|
+
import { Key, matchesKey } from "./text-helpers.js";
|
|
29
34
|
import type {
|
|
30
35
|
StageControlHandle,
|
|
31
36
|
StageControlRegistry,
|
|
32
37
|
} from "../runs/foreground/stage-control-registry.js";
|
|
33
38
|
import type { StageUiBroker } from "../shared/stage-ui-broker.js";
|
|
39
|
+
import type { StageSnapshot, StoreSnapshot } from "../shared/store-types.js";
|
|
40
|
+
import { expandWorkflowGraph } from "../shared/expanded-workflow-graph.js";
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
43
|
* Surface used to write Pi's footer/status tag while the attach pane is
|
|
@@ -112,11 +119,14 @@ export interface WorkflowAttachPaneOpts {
|
|
|
112
119
|
* scrolling and drops non-wheel mouse bytes before they reach the editor.
|
|
113
120
|
*/
|
|
114
121
|
setMouseScrollTracking?: (enabled: boolean) => void;
|
|
122
|
+
/** Optional clock injection for deterministic transition-quarantine tests. */
|
|
123
|
+
now?: () => number;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
export type WorkflowAttachPaneMode = "graph" | "stage-chat";
|
|
118
127
|
|
|
119
128
|
const STATUS_KEY = "pi-workflows";
|
|
129
|
+
const ENTER_TRANSITION_QUARANTINE_MS = 200;
|
|
120
130
|
|
|
121
131
|
export class WorkflowAttachPane implements Component {
|
|
122
132
|
private store: Store;
|
|
@@ -139,12 +149,25 @@ export class WorkflowAttachPane implements Component {
|
|
|
139
149
|
private piEditorFactory?: (tui: TUI, theme: EditorTheme, keybindings: unknown) => EditorComponent;
|
|
140
150
|
private getChatRenderSettings?: () => Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">> | undefined;
|
|
141
151
|
private footerData?: ReadonlyFooterDataProvider;
|
|
152
|
+
private now: () => number;
|
|
142
153
|
|
|
143
154
|
private mode: WorkflowAttachPaneMode = "graph";
|
|
155
|
+
private visible = true;
|
|
144
156
|
private graphView: GraphView;
|
|
145
157
|
private chatView: StageChatView | null = null;
|
|
158
|
+
private unsubscribeStore: (() => void) | null = null;
|
|
159
|
+
/** Run id for the currently attached stage chat; graph mode keeps `runId` as the root graph run. */
|
|
160
|
+
private attachedRunId: string | null = null;
|
|
146
161
|
/** Stage id the user most recently attached to (used to seed focus). */
|
|
147
162
|
private lastAttachedStageId: string | null = null;
|
|
163
|
+
/** Time-boxed guard for Enter leaking into graph mode during connect/detach transitions. */
|
|
164
|
+
private graphEnterQuarantineUntil = 0;
|
|
165
|
+
/** Time-boxed guard for Enter leaking from graph attach into an attached prompt. */
|
|
166
|
+
private stagePromptEnterQuarantineUntil = 0;
|
|
167
|
+
/** Last awaiting-input identity observed for graph-mode transition quarantine. */
|
|
168
|
+
private lastGraphAwaitingInputKey: string | null = null;
|
|
169
|
+
/** Last awaiting-input identity observed for attached-stage transition quarantine. */
|
|
170
|
+
private lastStageAwaitingInputKey: string | null = null;
|
|
148
171
|
|
|
149
172
|
constructor(opts: WorkflowAttachPaneOpts) {
|
|
150
173
|
this.store = opts.store;
|
|
@@ -167,12 +190,17 @@ export class WorkflowAttachPane implements Component {
|
|
|
167
190
|
this.piEditorFactory = opts.piEditorFactory;
|
|
168
191
|
this.getChatRenderSettings = opts.getChatRenderSettings;
|
|
169
192
|
this.footerData = opts.footerData;
|
|
193
|
+
this.now = opts.now ?? Date.now;
|
|
170
194
|
|
|
195
|
+
this.unsubscribeStore = this.store.subscribe((snapshot) => this._handleStoreUpdate(snapshot));
|
|
171
196
|
this.graphView = this._buildGraphView();
|
|
172
197
|
|
|
173
198
|
if (opts.initialAttachStageId !== undefined && this.runId) {
|
|
174
|
-
this.
|
|
199
|
+
const target = this._resolveGraphStageTarget(this.runId, opts.initialAttachStageId);
|
|
200
|
+
this._attachToStage(target.runId, target.stageId);
|
|
175
201
|
} else {
|
|
202
|
+
this._syncAwaitingInputKeys(this.store.snapshot());
|
|
203
|
+
this._armGraphEnterQuarantineIfRunNeedsInput();
|
|
176
204
|
this._setBaseStatus();
|
|
177
205
|
this._syncMouseScrollTracking();
|
|
178
206
|
}
|
|
@@ -188,12 +216,15 @@ export class WorkflowAttachPane implements Component {
|
|
|
188
216
|
onHide: this.onHide,
|
|
189
217
|
onKill: this.onKill,
|
|
190
218
|
onPromptResolve: this.onPromptResolve,
|
|
191
|
-
onStageAttach: (runId, stageId) => this._attachToStage(runId, stageId
|
|
219
|
+
onStageAttach: (runId, stageId) => this._attachToStage(runId, stageId, {
|
|
220
|
+
suppressInitialPromptSubmit: true,
|
|
221
|
+
}),
|
|
192
222
|
onDetach: () => {
|
|
193
223
|
if (this.onHide) this.onHide();
|
|
194
224
|
},
|
|
195
225
|
initialFocusedStageId,
|
|
196
226
|
getViewportRows: this.getViewportRows,
|
|
227
|
+
piKeybindings: this.piKeybindings,
|
|
197
228
|
// Gate the host render tick on `graph` mode. While the chat view
|
|
198
229
|
// is attached, the GraphView is hidden behind the chat — firing
|
|
199
230
|
// pi-tui renders for a frame the user can't see is wasted work.
|
|
@@ -226,19 +257,33 @@ export class WorkflowAttachPane implements Component {
|
|
|
226
257
|
return run?.stages.find((s) => s.id === stageId)?.name ?? "stage";
|
|
227
258
|
}
|
|
228
259
|
|
|
229
|
-
private _attachToStage(
|
|
230
|
-
|
|
260
|
+
private _attachToStage(
|
|
261
|
+
runId: string,
|
|
262
|
+
stageId: string,
|
|
263
|
+
options: { suppressInitialPromptSubmit?: boolean } = { suppressInitialPromptSubmit: true },
|
|
264
|
+
): void {
|
|
265
|
+
this.graphEnterQuarantineUntil = 0;
|
|
266
|
+
const snapshot = this.store.snapshot();
|
|
267
|
+
const graphRunId = this._resolveRunId();
|
|
268
|
+
this.lastGraphAwaitingInputKey = graphRunId ? this._runAwaitingInputKey(snapshot, graphRunId) : null;
|
|
269
|
+
this.lastStageAwaitingInputKey = this._stageAwaitingInputKey(snapshot, runId, stageId);
|
|
270
|
+
this.stagePromptEnterQuarantineUntil =
|
|
271
|
+
options.suppressInitialPromptSubmit === true && this.lastStageAwaitingInputKey !== null
|
|
272
|
+
? this.now() + ENTER_TRANSITION_QUARANTINE_MS
|
|
273
|
+
: 0;
|
|
274
|
+
this.attachedRunId = runId;
|
|
231
275
|
this.lastAttachedStageId = stageId;
|
|
232
276
|
const handle: StageControlHandle | undefined = this.registry?.get(runId, stageId);
|
|
233
277
|
this.chatView?.dispose();
|
|
234
|
-
|
|
278
|
+
let chatView!: StageChatView;
|
|
279
|
+
chatView = new StageChatView({
|
|
235
280
|
store: this.store,
|
|
236
281
|
graphTheme: this.theme,
|
|
237
282
|
runId,
|
|
238
283
|
stageId,
|
|
239
284
|
workflowName: this._workflowName(runId),
|
|
240
285
|
handle,
|
|
241
|
-
onDetach: () => this._detachFromStage(),
|
|
286
|
+
onDetach: (reason, metadata) => this._detachFromStage(reason, metadata),
|
|
242
287
|
onClose: this.onClose,
|
|
243
288
|
requestRender: this.hostRequestRender,
|
|
244
289
|
requestFocus: this.hostRequestFocus,
|
|
@@ -250,50 +295,126 @@ export class WorkflowAttachPane implements Component {
|
|
|
250
295
|
footerData: this.footerData,
|
|
251
296
|
getViewportRows: this.getViewportRows,
|
|
252
297
|
stageUiBroker: this.stageUiBroker,
|
|
298
|
+
canSubmitPrompt: (candidateRunId, candidateStageId) => (
|
|
299
|
+
this.visible &&
|
|
300
|
+
this.mode === "stage-chat" &&
|
|
301
|
+
this.chatView === chatView &&
|
|
302
|
+
this.attachedRunId === candidateRunId &&
|
|
303
|
+
this.lastAttachedStageId === candidateStageId &&
|
|
304
|
+
this._isStageMarkedAttached(candidateRunId, candidateStageId)
|
|
305
|
+
),
|
|
253
306
|
});
|
|
254
|
-
this.
|
|
307
|
+
this.chatView = chatView;
|
|
255
308
|
this.mode = "stage-chat";
|
|
309
|
+
this.store.recordStageAttached(runId, stageId, this.visible);
|
|
256
310
|
this._setAttachedStatus(runId, stageId);
|
|
257
311
|
this._syncMouseScrollTracking();
|
|
258
312
|
}
|
|
259
313
|
|
|
260
|
-
private _detachFromStage(
|
|
261
|
-
|
|
262
|
-
|
|
314
|
+
private _detachFromStage(
|
|
315
|
+
reason: StageChatDetachReason = "user",
|
|
316
|
+
metadata: StageChatDetachMetadata = {},
|
|
317
|
+
): void {
|
|
318
|
+
if (this.chatView && this.attachedRunId && this.lastAttachedStageId) {
|
|
319
|
+
this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, false);
|
|
263
320
|
}
|
|
264
321
|
this.chatView?.dispose();
|
|
265
322
|
this.chatView = null;
|
|
323
|
+
this.attachedRunId = null;
|
|
266
324
|
// Rebuild graph view so the focused stage matches the node we
|
|
267
325
|
// were just attached to (mockup contract: cursor still on
|
|
268
326
|
// `review-a` after Ctrl+D detach).
|
|
269
327
|
this.graphView.dispose();
|
|
270
328
|
this.graphView = this._buildGraphView(this.lastAttachedStageId ?? undefined);
|
|
271
329
|
this.mode = "graph";
|
|
330
|
+
this.stagePromptEnterQuarantineUntil = 0;
|
|
331
|
+
this.lastStageAwaitingInputKey = null;
|
|
332
|
+
this.lastGraphAwaitingInputKey = this.runId ? this._runAwaitingInputKey(this.store.snapshot(), this.runId) : null;
|
|
333
|
+
this.graphEnterQuarantineUntil =
|
|
334
|
+
reason === "prompt-resolved" && metadata.suppressNextGraphSubmit === true
|
|
335
|
+
? this.now() + ENTER_TRANSITION_QUARANTINE_MS
|
|
336
|
+
: 0;
|
|
272
337
|
this._setBaseStatus();
|
|
273
338
|
this._syncMouseScrollTracking();
|
|
274
339
|
}
|
|
275
340
|
|
|
276
341
|
retarget(runId: string | null, stageId?: string): void {
|
|
277
|
-
if (this.chatView && this.
|
|
278
|
-
this.store.recordStageAttached(this.
|
|
342
|
+
if (this.chatView && this.attachedRunId && this.lastAttachedStageId) {
|
|
343
|
+
this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, false);
|
|
279
344
|
}
|
|
280
345
|
this.chatView?.dispose();
|
|
281
346
|
this.chatView = null;
|
|
282
347
|
this.graphView.dispose();
|
|
283
348
|
this.runId = runId;
|
|
349
|
+
this.attachedRunId = null;
|
|
284
350
|
this.lastAttachedStageId = null;
|
|
285
351
|
this.mode = "graph";
|
|
352
|
+
this.graphEnterQuarantineUntil = 0;
|
|
353
|
+
this.stagePromptEnterQuarantineUntil = 0;
|
|
286
354
|
this.graphView = this._buildGraphView();
|
|
355
|
+
this._syncAwaitingInputKeys(this.store.snapshot());
|
|
287
356
|
|
|
288
357
|
if (stageId !== undefined && runId) {
|
|
289
|
-
this.
|
|
358
|
+
const target = this._resolveGraphStageTarget(runId, stageId);
|
|
359
|
+
this._attachToStage(target.runId, target.stageId);
|
|
290
360
|
return;
|
|
291
361
|
}
|
|
292
362
|
|
|
363
|
+
this._armGraphEnterQuarantineIfRunNeedsInput();
|
|
293
364
|
this._setBaseStatus();
|
|
294
365
|
this._syncMouseScrollTracking();
|
|
295
366
|
}
|
|
296
367
|
|
|
368
|
+
private _resolveGraphStageTarget(rootRunId: string, stageId: string): { runId: string; stageId: string } {
|
|
369
|
+
const graph = expandWorkflowGraph(this.store.snapshot(), rootRunId);
|
|
370
|
+
const match = graph.stages.find((stage) =>
|
|
371
|
+
stage.id === stageId || stage.workflowGraphTarget.stageId === stageId,
|
|
372
|
+
);
|
|
373
|
+
if (match === undefined) return { runId: rootRunId, stageId };
|
|
374
|
+
return {
|
|
375
|
+
runId: match.workflowGraphTarget.runId,
|
|
376
|
+
stageId: match.workflowGraphTarget.stageId,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private _handleStoreUpdate(snapshot: StoreSnapshot): void {
|
|
381
|
+
if (!this.visible) {
|
|
382
|
+
this._syncAwaitingInputKeys(snapshot);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const runId = this._resolveRunId();
|
|
386
|
+
if (!runId) {
|
|
387
|
+
this.lastGraphAwaitingInputKey = null;
|
|
388
|
+
this.lastStageAwaitingInputKey = null;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (this.mode === "graph") {
|
|
392
|
+
const key = this._runAwaitingInputKey(snapshot, runId);
|
|
393
|
+
if (key !== null && key !== this.lastGraphAwaitingInputKey) {
|
|
394
|
+
this.graphEnterQuarantineUntil = this.now() + ENTER_TRANSITION_QUARANTINE_MS;
|
|
395
|
+
}
|
|
396
|
+
this.lastGraphAwaitingInputKey = key;
|
|
397
|
+
this.lastStageAwaitingInputKey = null;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (this.mode === "stage-chat" && this.attachedRunId && this.lastAttachedStageId) {
|
|
401
|
+
const key = this._stageAwaitingInputKey(snapshot, this.attachedRunId, this.lastAttachedStageId);
|
|
402
|
+
if (key !== null && key !== this.lastStageAwaitingInputKey) {
|
|
403
|
+
this.stagePromptEnterQuarantineUntil = this.now() + ENTER_TRANSITION_QUARANTINE_MS;
|
|
404
|
+
}
|
|
405
|
+
this.lastStageAwaitingInputKey = key;
|
|
406
|
+
this.lastGraphAwaitingInputKey = this._runAwaitingInputKey(snapshot, runId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private _syncAwaitingInputKeys(snapshot: StoreSnapshot): void {
|
|
411
|
+
const runId = this._resolveRunId();
|
|
412
|
+
this.lastGraphAwaitingInputKey = runId ? this._runAwaitingInputKey(snapshot, runId) : null;
|
|
413
|
+
this.lastStageAwaitingInputKey = this.attachedRunId && this.lastAttachedStageId
|
|
414
|
+
? this._stageAwaitingInputKey(snapshot, this.attachedRunId, this.lastAttachedStageId)
|
|
415
|
+
: null;
|
|
416
|
+
}
|
|
417
|
+
|
|
297
418
|
private _setBaseStatus(): void {
|
|
298
419
|
const runId = this._resolveRunId();
|
|
299
420
|
const name = runId ? `pi-workflows/${this._workflowName(runId)}` : "pi-workflows";
|
|
@@ -306,9 +427,10 @@ export class WorkflowAttachPane implements Component {
|
|
|
306
427
|
}
|
|
307
428
|
|
|
308
429
|
setVisible(visible: boolean): void {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
430
|
+
this.visible = visible;
|
|
431
|
+
if (this.mode === "stage-chat" && this.attachedRunId && this.lastAttachedStageId) {
|
|
432
|
+
this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, visible);
|
|
433
|
+
if (visible) this._setAttachedStatus(this.attachedRunId, this.lastAttachedStageId);
|
|
312
434
|
else this.uiStatus?.setStatus?.(STATUS_KEY, undefined);
|
|
313
435
|
return;
|
|
314
436
|
}
|
|
@@ -327,6 +449,21 @@ export class WorkflowAttachPane implements Component {
|
|
|
327
449
|
return this.mode === "graph";
|
|
328
450
|
}
|
|
329
451
|
|
|
452
|
+
wantsFocusForAwaitingInput(snapshot: StoreSnapshot): boolean {
|
|
453
|
+
if (!this.visible) return false;
|
|
454
|
+
const runId = this._resolveRunId();
|
|
455
|
+
if (!runId) return false;
|
|
456
|
+
const run = snapshot.runs.find((candidate) => candidate.id === runId);
|
|
457
|
+
if (!run) return false;
|
|
458
|
+
if (this.mode === "graph") {
|
|
459
|
+
return this._runAwaitingInputKey(snapshot, runId) !== null;
|
|
460
|
+
}
|
|
461
|
+
if (this.mode !== "stage-chat" || !this.attachedRunId || !this.lastAttachedStageId) return false;
|
|
462
|
+
const attachedRun = snapshot.runs.find((candidate) => candidate.id === this.attachedRunId);
|
|
463
|
+
const stage = attachedRun?.stages.find((candidate) => candidate.id === this.lastAttachedStageId);
|
|
464
|
+
return stage?.attached === true && this._stageSnapshotNeedsInput(stage);
|
|
465
|
+
}
|
|
466
|
+
|
|
330
467
|
render(width: number): string[] {
|
|
331
468
|
if (this.mode === "stage-chat" && this.chatView) {
|
|
332
469
|
return this.chatView.render(width);
|
|
@@ -335,20 +472,134 @@ export class WorkflowAttachPane implements Component {
|
|
|
335
472
|
}
|
|
336
473
|
|
|
337
474
|
handleInput(data: string): boolean | void {
|
|
475
|
+
if (!this.visible) return false;
|
|
338
476
|
if (this.mode === "stage-chat" && this.chatView) {
|
|
477
|
+
if (this._shouldQuarantineStagePromptEnter(data)) return true;
|
|
339
478
|
return this.chatView.handleInput(data);
|
|
340
479
|
}
|
|
480
|
+
if (this._shouldQuarantineGraphEnter(data)) return true;
|
|
341
481
|
return this.graphView.handleInput(data);
|
|
342
482
|
}
|
|
343
483
|
|
|
484
|
+
private _shouldQuarantineStagePromptEnter(data: string): boolean {
|
|
485
|
+
if (this.stagePromptEnterQuarantineUntil <= 0) return false;
|
|
486
|
+
if (!matchesKey(data, Key.enter)) {
|
|
487
|
+
this.stagePromptEnterQuarantineUntil = 0;
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
if (!this.attachedRunId || !this.lastAttachedStageId) {
|
|
491
|
+
this.stagePromptEnterQuarantineUntil = 0;
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
if (!this._stageNeedsInput(this.attachedRunId, this.lastAttachedStageId)) {
|
|
495
|
+
this.stagePromptEnterQuarantineUntil = 0;
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
const now = this.now();
|
|
499
|
+
if (now <= this.stagePromptEnterQuarantineUntil) {
|
|
500
|
+
this.stagePromptEnterQuarantineUntil = now + ENTER_TRANSITION_QUARANTINE_MS;
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
this.stagePromptEnterQuarantineUntil = 0;
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private _shouldQuarantineGraphEnter(data: string): boolean {
|
|
508
|
+
if (this.graphEnterQuarantineUntil <= 0) return false;
|
|
509
|
+
if (!matchesKey(data, Key.enter)) {
|
|
510
|
+
this.graphEnterQuarantineUntil = 0;
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
const now = this.now();
|
|
514
|
+
if (now <= this.graphEnterQuarantineUntil) {
|
|
515
|
+
this.graphEnterQuarantineUntil = now + ENTER_TRANSITION_QUARANTINE_MS;
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
this.graphEnterQuarantineUntil = 0;
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private _armGraphEnterQuarantineIfRunNeedsInput(): void {
|
|
523
|
+
const runId = this._resolveRunId();
|
|
524
|
+
this.graphEnterQuarantineUntil = runId && this._runNeedsInput(runId)
|
|
525
|
+
? this.now() + ENTER_TRANSITION_QUARANTINE_MS
|
|
526
|
+
: 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private _runNeedsInput(runId: string): boolean {
|
|
530
|
+
return this._runAwaitingInputKey(this.store.snapshot(), runId) !== null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private _runAwaitingInputKey(snapshot: StoreSnapshot, runId: string): string | null {
|
|
534
|
+
const run = snapshot.runs.find((candidate) => candidate.id === runId);
|
|
535
|
+
if (!run) return null;
|
|
536
|
+
const keys: Array<{ key: string; createdAt: number }> = [];
|
|
537
|
+
if (run.pendingPrompt) {
|
|
538
|
+
keys.push({ key: `run-prompt:${run.pendingPrompt.id}`, createdAt: run.pendingPrompt.createdAt });
|
|
539
|
+
}
|
|
540
|
+
const graph = expandWorkflowGraph(snapshot, runId);
|
|
541
|
+
for (const stage of graph.stages) {
|
|
542
|
+
const key = this._stageAwaitingInputKeyFromSnapshot(stage);
|
|
543
|
+
if (key) keys.push(key);
|
|
544
|
+
}
|
|
545
|
+
if (keys.length === 0) return null;
|
|
546
|
+
keys.sort((a, b) => b.createdAt - a.createdAt);
|
|
547
|
+
return keys[0]!.key;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private _stageNeedsInput(runId: string, stageId: string): boolean {
|
|
551
|
+
return this._stageAwaitingInputKey(this.store.snapshot(), runId, stageId) !== null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private _stageAwaitingInputKey(snapshot: StoreSnapshot, runId: string, stageId: string): string | null {
|
|
555
|
+
const run = snapshot.runs.find((candidate) => candidate.id === runId);
|
|
556
|
+
const stage = run?.stages.find((candidate) => candidate.id === stageId);
|
|
557
|
+
return stage ? this._stageAwaitingInputKeyFromSnapshot(stage)?.key ?? null : null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private _stageAwaitingInputKeyFromSnapshot(stage: StageSnapshot): { key: string; createdAt: number } | null {
|
|
561
|
+
if (stage.pendingPrompt) {
|
|
562
|
+
return { key: `stage-prompt:${stage.id}:${stage.pendingPrompt.id}`, createdAt: stage.pendingPrompt.createdAt };
|
|
563
|
+
}
|
|
564
|
+
if (stage.inputRequest) {
|
|
565
|
+
return { key: `stage-input:${stage.id}:${stage.inputRequest.id}`, createdAt: stage.inputRequest.createdAt };
|
|
566
|
+
}
|
|
567
|
+
if (stage.status === "awaiting_input") {
|
|
568
|
+
return {
|
|
569
|
+
key: `stage-awaiting:${stage.id}:${stage.awaitingInputSince ?? "active"}`,
|
|
570
|
+
createdAt: stage.awaitingInputSince ?? stage.startedAt ?? 0,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private _isStageMarkedAttached(runId: string, stageId: string): boolean {
|
|
577
|
+
return this._stageSnapshot(runId, stageId)?.attached === true;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private _stageSnapshot(runId: string, stageId: string): StageSnapshot | undefined {
|
|
581
|
+
const run = this.store.snapshot().runs.find((candidate) => candidate.id === runId);
|
|
582
|
+
return run?.stages.find((candidate) => candidate.id === stageId);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private _stageSnapshotNeedsInput(stage: Pick<StageSnapshot, "pendingPrompt" | "inputRequest" | "status">): boolean {
|
|
586
|
+
return (
|
|
587
|
+
stage.pendingPrompt !== undefined ||
|
|
588
|
+
stage.inputRequest !== undefined ||
|
|
589
|
+
stage.status === "awaiting_input"
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
344
593
|
invalidate(): void {
|
|
345
594
|
if (this.mode === "stage-chat" && this.chatView) this.chatView.invalidate();
|
|
346
595
|
else this.graphView.invalidate();
|
|
347
596
|
}
|
|
348
597
|
|
|
349
598
|
dispose(): void {
|
|
350
|
-
|
|
351
|
-
|
|
599
|
+
this.unsubscribeStore?.();
|
|
600
|
+
this.unsubscribeStore = null;
|
|
601
|
+
if (this.chatView && this.attachedRunId && this.lastAttachedStageId) {
|
|
602
|
+
this.store.recordStageAttached(this.attachedRunId, this.lastAttachedStageId, false);
|
|
352
603
|
}
|
|
353
604
|
this.chatView?.dispose();
|
|
354
605
|
this.chatView = null;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { renderRoundedBoxLines } from "./chat-surface.js";
|
|
2
|
+
import { BOLD, RESET, hexToAnsi } from "./color-utils.js";
|
|
3
|
+
import type { GraphTheme } from "./graph-theme.js";
|
|
4
|
+
import { truncateToWidth, visibleWidth, wrapPlainText } from "./text-helpers.js";
|
|
5
|
+
|
|
6
|
+
export type WorkflowNoticeTone = "info" | "success" | "warning" | "error" | "mauve";
|
|
7
|
+
|
|
8
|
+
export interface WorkflowNoticeCardField {
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly value: string | undefined;
|
|
11
|
+
readonly tone?: WorkflowNoticeTone | "text" | "muted";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WorkflowNoticeCardOpts {
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly glyph: string;
|
|
17
|
+
readonly headline: string;
|
|
18
|
+
readonly tone: WorkflowNoticeTone;
|
|
19
|
+
readonly fields?: readonly WorkflowNoticeCardField[];
|
|
20
|
+
readonly hints?: readonly string[];
|
|
21
|
+
readonly footer?: string;
|
|
22
|
+
readonly fallbackText: string;
|
|
23
|
+
readonly width: number;
|
|
24
|
+
readonly theme?: GraphTheme;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MIN_CARD_WIDTH = 32;
|
|
28
|
+
const FIELD_LABEL_WIDTH = 9;
|
|
29
|
+
|
|
30
|
+
export function renderWorkflowNoticeCard(opts: WorkflowNoticeCardOpts): string[] {
|
|
31
|
+
const width = Math.max(1, Math.floor(opts.width));
|
|
32
|
+
if (width < MIN_CARD_WIDTH) return wrapPlainText(opts.fallbackText, width);
|
|
33
|
+
|
|
34
|
+
const theme = opts.theme;
|
|
35
|
+
const accent = theme ? toneColor(theme, opts.tone) : undefined;
|
|
36
|
+
const innerWidth = Math.max(2, width - 2);
|
|
37
|
+
const bodyLines: string[] = [];
|
|
38
|
+
|
|
39
|
+
appendHeadline(bodyLines, opts, innerWidth);
|
|
40
|
+
|
|
41
|
+
for (const field of opts.fields ?? []) {
|
|
42
|
+
if (field.value === undefined || field.value.length === 0) continue;
|
|
43
|
+
appendField(bodyLines, field, innerWidth, theme);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const hint of opts.hints ?? []) {
|
|
47
|
+
if (hint.length === 0) continue;
|
|
48
|
+
appendHint(bodyLines, hint, innerWidth, theme);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (opts.footer && opts.footer.length > 0) {
|
|
52
|
+
appendFreeText(bodyLines, opts.footer, innerWidth, theme, "muted");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return renderRoundedBoxLines({
|
|
56
|
+
title: opts.title,
|
|
57
|
+
bodyLines,
|
|
58
|
+
width,
|
|
59
|
+
...(theme ? { theme, accent } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendHeadline(
|
|
64
|
+
rows: string[],
|
|
65
|
+
opts: WorkflowNoticeCardOpts,
|
|
66
|
+
innerWidth: number,
|
|
67
|
+
): void {
|
|
68
|
+
const prefix = `${opts.glyph} `;
|
|
69
|
+
const continuationPrefix = `${" ".repeat(visibleWidth(opts.glyph))} `;
|
|
70
|
+
const budget = Math.max(1, innerWidth - 1 - visibleWidth(prefix));
|
|
71
|
+
const lines = wrapPlainText(opts.headline, budget);
|
|
72
|
+
const theme = opts.theme;
|
|
73
|
+
const tone = theme ? toneColor(theme, opts.tone) : undefined;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const rawPrefix = i === 0 ? prefix : continuationPrefix;
|
|
77
|
+
const styledPrefix = i === 0
|
|
78
|
+
? style(rawPrefix, theme, tone, true)
|
|
79
|
+
: " ".repeat(visibleWidth(rawPrefix));
|
|
80
|
+
const line = ` ${styledPrefix}${style(lines[i] ?? "", theme, theme?.text, true)}`;
|
|
81
|
+
rows.push(fit(line, innerWidth));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendField(
|
|
86
|
+
rows: string[],
|
|
87
|
+
field: WorkflowNoticeCardField,
|
|
88
|
+
innerWidth: number,
|
|
89
|
+
theme: GraphTheme | undefined,
|
|
90
|
+
): void {
|
|
91
|
+
const label = truncateToWidth(field.label, FIELD_LABEL_WIDTH, "…").padEnd(FIELD_LABEL_WIDTH, " ");
|
|
92
|
+
const firstPrefixWidth = 1 + FIELD_LABEL_WIDTH + 1;
|
|
93
|
+
const firstBudget = Math.max(1, innerWidth - firstPrefixWidth);
|
|
94
|
+
const value = field.value ?? "";
|
|
95
|
+
const valueTone = field.tone ?? "text";
|
|
96
|
+
|
|
97
|
+
if (visibleWidth(value) <= firstBudget && !value.includes("\n")) {
|
|
98
|
+
rows.push(
|
|
99
|
+
fit(
|
|
100
|
+
` ${style(label, theme, theme?.dim, true)} ${style(value, theme, colorForField(theme, valueTone))}`,
|
|
101
|
+
innerWidth,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
rows.push(fit(` ${style(field.label, theme, theme?.dim, true)}`, innerWidth));
|
|
108
|
+
if (visibleWidth(value) <= innerWidth - 1 && !value.includes("\n")) {
|
|
109
|
+
rows.push(fit(` ${style(value, theme, colorForField(theme, valueTone))}`, innerWidth));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const continuationPrefix = " ";
|
|
113
|
+
const continuationBudget = Math.max(1, innerWidth - visibleWidth(continuationPrefix));
|
|
114
|
+
for (const line of wrapPlainText(value, continuationBudget)) {
|
|
115
|
+
rows.push(fit(`${continuationPrefix}${style(line, theme, colorForField(theme, valueTone))}`, innerWidth));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function appendHint(
|
|
120
|
+
rows: string[],
|
|
121
|
+
hint: string,
|
|
122
|
+
innerWidth: number,
|
|
123
|
+
theme: GraphTheme | undefined,
|
|
124
|
+
): void {
|
|
125
|
+
const prefix = "▸ ";
|
|
126
|
+
const continuationPrefix = " ";
|
|
127
|
+
const budget = Math.max(1, innerWidth - 1 - visibleWidth(prefix));
|
|
128
|
+
const lines = wrapPlainText(hint, budget);
|
|
129
|
+
for (let i = 0; i < lines.length; i++) {
|
|
130
|
+
const rawPrefix = i === 0 ? prefix : continuationPrefix;
|
|
131
|
+
const styledPrefix = i === 0
|
|
132
|
+
? style(rawPrefix, theme, theme?.accent, true)
|
|
133
|
+
: " ".repeat(visibleWidth(rawPrefix));
|
|
134
|
+
rows.push(fit(` ${styledPrefix}${style(lines[i] ?? "", theme, theme?.textMuted)}`, innerWidth));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function appendFreeText(
|
|
139
|
+
rows: string[],
|
|
140
|
+
text: string,
|
|
141
|
+
innerWidth: number,
|
|
142
|
+
theme: GraphTheme | undefined,
|
|
143
|
+
tone: WorkflowNoticeCardField["tone"],
|
|
144
|
+
): void {
|
|
145
|
+
const budget = Math.max(1, innerWidth - 1);
|
|
146
|
+
for (const line of wrapPlainText(text, budget)) {
|
|
147
|
+
rows.push(fit(` ${style(line, theme, colorForField(theme, tone ?? "text"))}`, innerWidth));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function colorForField(
|
|
152
|
+
theme: GraphTheme | undefined,
|
|
153
|
+
tone: WorkflowNoticeCardField["tone"],
|
|
154
|
+
): string | undefined {
|
|
155
|
+
if (!theme) return undefined;
|
|
156
|
+
if (tone === "text") return theme.text;
|
|
157
|
+
if (tone === "muted") return theme.textMuted;
|
|
158
|
+
return toneColor(theme, tone ?? "info");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toneColor(theme: GraphTheme, tone: WorkflowNoticeTone): string {
|
|
162
|
+
switch (tone) {
|
|
163
|
+
case "success":
|
|
164
|
+
return theme.success;
|
|
165
|
+
case "warning":
|
|
166
|
+
return theme.warning;
|
|
167
|
+
case "error":
|
|
168
|
+
return theme.error;
|
|
169
|
+
case "mauve":
|
|
170
|
+
return theme.mauve;
|
|
171
|
+
case "info":
|
|
172
|
+
default:
|
|
173
|
+
return theme.info;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function style(text: string, theme: GraphTheme | undefined, color: string | undefined, bold = false): string {
|
|
178
|
+
if (!theme || !color || text.length === 0) return text;
|
|
179
|
+
return `${hexToAnsi(color)}${bold ? BOLD : ""}${text}${RESET}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fit(line: string, width: number): string {
|
|
183
|
+
return truncateToWidth(line, width, "…", true);
|
|
184
|
+
}
|