avtc-pi-featyard 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/agents/fy-design-reviewer.md +77 -0
- package/agents/fy-feature-verifier.md +24 -0
- package/agents/fy-general-reviewer.md +81 -0
- package/agents/fy-guidelines-reviewer.md +46 -0
- package/agents/fy-implementer.md +20 -0
- package/agents/fy-performance-reviewer.md +46 -0
- package/agents/fy-plan-reviewer.md +74 -0
- package/agents/fy-plan-verifier.md +44 -0
- package/agents/fy-quality-reviewer.md +50 -0
- package/agents/fy-requirements-reviewer.md +46 -0
- package/agents/fy-researcher.md +6 -0
- package/agents/fy-security-reviewer.md +46 -0
- package/agents/fy-task-verifier.md +42 -0
- package/agents/fy-testing-reviewer.md +60 -0
- package/assets/images/kanban-board.png +0 -0
- package/assets/images/settings-dropdown.png +0 -0
- package/assets/images/settings.png +0 -0
- package/assets/images/widget.png +0 -0
- package/docs/CONFIGURATION.md +266 -0
- package/index.ts +8 -0
- package/package.json +85 -0
- package/skills/fy-design/SKILL.md +108 -0
- package/skills/fy-design-review/SKILL.md +45 -0
- package/skills/fy-finish/SKILL.md +28 -0
- package/skills/fy-implement/SKILL.md +36 -0
- package/skills/fy-plan/SKILL.md +77 -0
- package/skills/fy-plan-review/SKILL.md +45 -0
- package/skills/fy-research/SKILL.md +38 -0
- package/skills/fy-review/SKILL.md +88 -0
- package/skills/fy-verify/SKILL.md +23 -0
- package/src/commands/auto-agent-commands.ts +748 -0
- package/src/commands/kanban-commands.ts +278 -0
- package/src/commands/workflow-commands.ts +531 -0
- package/src/compaction/compact-handler.ts +371 -0
- package/src/compaction/compact-message.ts +52 -0
- package/src/compaction/compact-trigger.ts +121 -0
- package/src/compaction/safe-editor-write.ts +18 -0
- package/src/events/agent/agent-end.ts +19 -0
- package/src/events/agent/agent-settled.ts +20 -0
- package/src/events/agent/agent-start.ts +18 -0
- package/src/events/agent/turn-end.ts +20 -0
- package/src/events/agent/turn-start.ts +19 -0
- package/src/events/event-deps.ts +33 -0
- package/src/events/index.ts +91 -0
- package/src/events/input/input.ts +143 -0
- package/src/events/session/context.ts +68 -0
- package/src/events/session/model-select.ts +20 -0
- package/src/events/session/session-compact.ts +20 -0
- package/src/events/session/session-shutdown.ts +26 -0
- package/src/events/session/session-start.ts +45 -0
- package/src/events/session/session-tree.ts +17 -0
- package/src/events/tool/tool-call.ts +58 -0
- package/src/events/tool/tool-result.ts +54 -0
- package/src/git/git-queries.ts +154 -0
- package/src/git/resolve-base-branch.ts +173 -0
- package/src/git/worktrees/worktree-helpers.ts +142 -0
- package/src/git/worktrees/worktree-interception.ts +66 -0
- package/src/git/worktrees/worktree-lifecycle.ts +380 -0
- package/src/guardrails/completion-gating.ts +63 -0
- package/src/guardrails/file-classifier.ts +461 -0
- package/src/guardrails/force-add-guard.ts +312 -0
- package/src/guardrails/guardrail-tracker.ts +73 -0
- package/src/guardrails/guardrail-violations.ts +47 -0
- package/src/guardrails/guardrails-engine.ts +663 -0
- package/src/guardrails/session-guardrails.ts +75 -0
- package/src/guardrails/shell-decompose.ts +285 -0
- package/src/guardrails/tdd-enforcement.ts +122 -0
- package/src/guardrails/test-output.ts +272 -0
- package/src/index.ts +598 -0
- package/src/integrations/parallel-work-guardrail-integration.ts +29 -0
- package/src/integrations/subagent-integration.ts +166 -0
- package/src/integrations/todo-integration.ts +58 -0
- package/src/kanban/auto-agent/auto-agent-cleanup.ts +37 -0
- package/src/kanban/auto-agent/auto-agent-events.ts +142 -0
- package/src/kanban/auto-agent/auto-agent-grace-period.ts +83 -0
- package/src/kanban/auto-agent/auto-agent-lifecycle.ts +497 -0
- package/src/kanban/auto-agent/auto-agent-notify.ts +43 -0
- package/src/kanban/auto-agent/auto-agent-pause.ts +44 -0
- package/src/kanban/auto-agent/auto-agent-state-machine.ts +629 -0
- package/src/kanban/data/kanban-database.ts +851 -0
- package/src/kanban/data/kanban-db-schema.ts +137 -0
- package/src/kanban/data/kanban-detect-project.ts +43 -0
- package/src/kanban/data/kanban-move-feature.ts +43 -0
- package/src/kanban/data/kanban-types.ts +64 -0
- package/src/kanban/ensure-feature.ts +226 -0
- package/src/kanban/kanban-board-ui/app.js +1125 -0
- package/src/kanban/kanban-board-ui/csv-parser.d.ts +5 -0
- package/src/kanban/kanban-board-ui/csv-parser.js +80 -0
- package/src/kanban/kanban-board-ui/index.html +127 -0
- package/src/kanban/kanban-board-ui/style.css +820 -0
- package/src/kanban/kanban-bridge.ts +216 -0
- package/src/kanban/kanban-context.ts +43 -0
- package/src/kanban/kanban-events.ts +34 -0
- package/src/kanban/kanban-generate-title.ts +259 -0
- package/src/kanban/kanban-generate-topic.ts +170 -0
- package/src/kanban/kanban-operations.ts +170 -0
- package/src/kanban/kanban-server.ts +931 -0
- package/src/kanban/kanban-turn-handlers.ts +55 -0
- package/src/kanban/model-capture.ts +34 -0
- package/src/log.ts +47 -0
- package/src/phases/env-sync.ts +64 -0
- package/src/phases/execution-mode.ts +79 -0
- package/src/phases/implement-to-verify.ts +31 -0
- package/src/phases/phase-progression.ts +249 -0
- package/src/phases/phase-transitions.ts +226 -0
- package/src/phases/workflow-router.ts +98 -0
- package/src/prompts/finish-instructions.ts +143 -0
- package/src/prompts/skill-block-builder.ts +73 -0
- package/src/prompts/skill-expansion.ts +262 -0
- package/src/prompts/task-gate-skill.ts +83 -0
- package/src/prompts/template-engine.ts +433 -0
- package/src/prompts/text-blocks.ts +279 -0
- package/src/review/review-context.ts +56 -0
- package/src/review/review-counter.ts +66 -0
- package/src/review/review-empty-loop-tracking.ts +52 -0
- package/src/review/review-loops.ts +217 -0
- package/src/review/review-report.ts +101 -0
- package/src/settings/model-overrides.ts +269 -0
- package/src/settings/settings-schema.ts +458 -0
- package/src/settings/settings-types.ts +81 -0
- package/src/settings/settings-ui.ts +222 -0
- package/src/shared/errors.ts +24 -0
- package/src/shared/global.d.ts +22 -0
- package/src/shared/session-entries.ts +24 -0
- package/src/shared/types.ts +195 -0
- package/src/shared/workflow-refs.ts +253 -0
- package/src/shared/workflow-types.ts +180 -0
- package/src/snippets/vendored/subscribe-to-dialog-coordinator.ts +98 -0
- package/src/snippets/vendored/subscribe-to-notifications.ts +150 -0
- package/src/snippets/vendored/subscribe-to-parallel-work-guardrail.ts +35 -0
- package/src/snippets/vendored/subscribe-to-subagent.ts +67 -0
- package/src/snippets/vendored/subscribe-to-todo.ts +63 -0
- package/src/state/archive-artifacts.ts +602 -0
- package/src/state/artifact-junction.ts +462 -0
- package/src/state/artifact-paths.ts +230 -0
- package/src/state/feature-activation.ts +239 -0
- package/src/state/feature-management.ts +325 -0
- package/src/state/feature-record-store.ts +39 -0
- package/src/state/feature-session.ts +263 -0
- package/src/state/feature-state.ts +439 -0
- package/src/state/post-turn-dispatch.ts +106 -0
- package/src/state/session-lifecycle.ts +346 -0
- package/src/state/state-persistence.ts +122 -0
- package/src/state/worth-notes.ts +89 -0
- package/src/tools/add-to-backlog.ts +117 -0
- package/src/tools/phase-ready.ts +742 -0
- package/src/tools/task-ready-advance.ts +235 -0
- package/src/tools/text-result.ts +15 -0
- package/src/ui/featyard-widget.ts +250 -0
- package/src/ui/manage-features-dialog.ts +246 -0
- package/src/ui/select-dialog.ts +27 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Orchestrator refs and closure-wrapping functions.
|
|
6
|
+
*
|
|
7
|
+
* Module-level refs that child modules need to access without creating
|
|
8
|
+
* circular imports back to the parent orchestrator.
|
|
9
|
+
*
|
|
10
|
+
* The parent orchestrator sets these refs during initialization.
|
|
11
|
+
* Child modules read them to access handler state.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { clearActiveFeatureEnv } from "../phases/env-sync.js";
|
|
16
|
+
import { applyModelOverride, getLoopCountForPhase } from "../phases/phase-transitions.js";
|
|
17
|
+
import type { SubstitutionResult } from "../prompts/skill-expansion.js";
|
|
18
|
+
import * as skillExpansion from "../prompts/skill-expansion.js";
|
|
19
|
+
import type { MutableRef } from "../shared/types.js";
|
|
20
|
+
import type { IGuardrails } from "../shared/workflow-types.js";
|
|
21
|
+
import { type FeatureSession, NO_ACTIVE_FEATURE_STATE } from "../state/feature-session.js";
|
|
22
|
+
import {
|
|
23
|
+
clearFeatureStateCache,
|
|
24
|
+
DEFAULT_DIR,
|
|
25
|
+
type FeatureState,
|
|
26
|
+
stateFilePath as featureStateFilePath,
|
|
27
|
+
} from "../state/feature-state.js";
|
|
28
|
+
|
|
29
|
+
// Re-export SubstitutionResult for consumers
|
|
30
|
+
export type { SubstitutionResult };
|
|
31
|
+
|
|
32
|
+
/** Pass as `featureStateOverride` when no override is needed. */
|
|
33
|
+
export const NO_FEATURE_STATE_OVERRIDE: FeatureState | null = null;
|
|
34
|
+
/** Pass as `agentName` when no specific agent name is known. */
|
|
35
|
+
export const NO_AGENT_NAME: string | null = null;
|
|
36
|
+
|
|
37
|
+
/** Grouped refs for empty-loop tracking — all set together from compaction instance. */
|
|
38
|
+
export interface EmptyLoopRefs {
|
|
39
|
+
getEmptyLoopsForSlug: (slug: string) => Record<string, number>;
|
|
40
|
+
incrementEmptyLoop: (slug: string, reviewerName: string) => void;
|
|
41
|
+
resetEmptyLoop: (slug: string, reviewerName: string) => void;
|
|
42
|
+
resetAllEmptyLoops: () => void;
|
|
43
|
+
isReviewerSkipped: (slug: string, reviewerName: string, threshold: number) => boolean;
|
|
44
|
+
getReviewerEmptyLoops: () => Record<string, Record<string, number>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Skill resolution ref — set from compaction instance. */
|
|
48
|
+
export interface SkillResolutionRef {
|
|
49
|
+
getExpectedSkill: () => string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Module-level refs ---
|
|
53
|
+
|
|
54
|
+
/** Handler reference — set by parent orchestrator during init */
|
|
55
|
+
let _handlerRef: FeatureSession | null = null;
|
|
56
|
+
|
|
57
|
+
/** Empty loop tracking refs — set as a group from compaction instance */
|
|
58
|
+
let _emptyLoops: EmptyLoopRefs | null = null;
|
|
59
|
+
|
|
60
|
+
/** Skill resolution ref — set from compaction instance */
|
|
61
|
+
let _skillResolution: SkillResolutionRef | null = null;
|
|
62
|
+
|
|
63
|
+
/** Agent state tracking ref */
|
|
64
|
+
const _agentJustFinishedRef: MutableRef<boolean> = { value: false };
|
|
65
|
+
|
|
66
|
+
/** Guardrails instance ref */
|
|
67
|
+
let _guardrailsRef: IGuardrails | null = null;
|
|
68
|
+
/** PhaseReady instance ref — for resetTracking access (test cleanup / fy:reset) */
|
|
69
|
+
let _phaseReadyRef: import("../shared/workflow-types.js").IPhaseReady | null = null;
|
|
70
|
+
let _getAutoAgentCallbackRef:
|
|
71
|
+
| (() => import("../kanban/auto-agent/auto-agent-state-machine.js").AutoAgentCallback | null)
|
|
72
|
+
| null = null;
|
|
73
|
+
|
|
74
|
+
// --- Ref setters (called by parent orchestrator) ---
|
|
75
|
+
|
|
76
|
+
export function setHandlerRef(handler: FeatureSession | null): void {
|
|
77
|
+
_handlerRef = handler;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Set empty-loop tracking refs from the compaction instance. */
|
|
81
|
+
export function setEmptyLoopRefs(refs: EmptyLoopRefs | null): void {
|
|
82
|
+
_emptyLoops = refs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Set skill resolution ref from the compaction instance. */
|
|
86
|
+
export function setSkillResolutionRef(refs: SkillResolutionRef | null): void {
|
|
87
|
+
_skillResolution = refs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function setGuardrailsRef(ref: IGuardrails | null): void {
|
|
91
|
+
_guardrailsRef = ref;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function setPhaseReadyRef(ref: import("../shared/workflow-types.js").IPhaseReady | null): void {
|
|
95
|
+
_phaseReadyRef = ref;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getPhaseReadyRef(): import("../shared/workflow-types.js").IPhaseReady | null {
|
|
99
|
+
return _phaseReadyRef;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function setAutoAgentCallbackRef(
|
|
103
|
+
ref: (() => import("../kanban/auto-agent/auto-agent-state-machine.js").AutoAgentCallback | null) | null,
|
|
104
|
+
): void {
|
|
105
|
+
_getAutoAgentCallbackRef = ref;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Ref getters ---
|
|
109
|
+
|
|
110
|
+
export function getHandlerRef(): FeatureSession | null {
|
|
111
|
+
return _handlerRef;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getActiveFeatureSlug(): string | null {
|
|
115
|
+
return _handlerRef?.getActiveFeatureSlug() ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getAgentJustFinishedRef(): MutableRef<boolean> {
|
|
119
|
+
return _agentJustFinishedRef;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getGuardrailsRef(): IGuardrails | null {
|
|
123
|
+
return _guardrailsRef;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Test-only accessors ---
|
|
127
|
+
|
|
128
|
+
/** @internal Returns the expected skill based on current workflow state */
|
|
129
|
+
export function _getExpectedSkill(): string | null {
|
|
130
|
+
return _skillResolution?.getExpectedSkill() ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @internal Increment empty loop count for a reviewer */
|
|
134
|
+
export function _incrementEmptyLoop(slug: string, reviewerName: string): void {
|
|
135
|
+
_emptyLoops?.incrementEmptyLoop(slug, reviewerName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @internal Reset empty loop count for a specific reviewer */
|
|
139
|
+
export function _resetEmptyLoop(slug: string, reviewerName: string): void {
|
|
140
|
+
_emptyLoops?.resetEmptyLoop(slug, reviewerName);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @internal Get empty loop counts for a feature slug */
|
|
144
|
+
export function _getEmptyLoopsForSlug(slug: string): Record<string, number> {
|
|
145
|
+
return _emptyLoops?.getEmptyLoopsForSlug(slug) ?? {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @internal Reset all empty loop tracking */
|
|
149
|
+
export function _resetAllEmptyLoops(): void {
|
|
150
|
+
_emptyLoops?.resetAllEmptyLoops();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @internal Check if a reviewer should be skipped based on threshold */
|
|
154
|
+
export function _isReviewerSkipped(slug: string, reviewerName: string, threshold: number): boolean {
|
|
155
|
+
return _emptyLoops?.isReviewerSkipped(slug, reviewerName, threshold) ?? false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @internal Get all empty loop counts (for template substitution) */
|
|
159
|
+
export function _getReviewerEmptyLoops(): Record<string, Record<string, number>> {
|
|
160
|
+
return _emptyLoops?.getReviewerEmptyLoops() ?? {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @internal Get state file path for current active feature */
|
|
164
|
+
export function getStateFilePath(): string | null {
|
|
165
|
+
const slug = getActiveFeatureSlug();
|
|
166
|
+
if (slug) {
|
|
167
|
+
return featureStateFilePath(slug, DEFAULT_DIR);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @internal Reset module-level state for testing */
|
|
173
|
+
export function _resetFeatureState(): void {
|
|
174
|
+
_handlerRef?.setActiveFeatureState(NO_ACTIVE_FEATURE_STATE);
|
|
175
|
+
if (globalThis.__piWorkflowMonitor) {
|
|
176
|
+
globalThis.__piWorkflowMonitor.workflowInitiatedNewSession = undefined;
|
|
177
|
+
globalThis.__piWorkflowMonitor.newSessionMessage = undefined;
|
|
178
|
+
globalThis.__piWorkflowMonitor.modelOverrideRefs = { pi: undefined };
|
|
179
|
+
// Clear the background-archive sweep interval. _resetFeatureState is the per-test cleanup
|
|
180
|
+
// most test files invoke in afterEach; with isolate:false a 24h setInterval left on the bridge
|
|
181
|
+
// by one activation keeps the event loop alive and shifts timing for later tests (exposing
|
|
182
|
+
// pre-existing ordering races). Clearing it here ensures no activation leaks a timer across
|
|
183
|
+
// tests. (This function is test-only — no production caller)
|
|
184
|
+
if (globalThis.__piWorkflowMonitor.archiveTimer) {
|
|
185
|
+
clearInterval(globalThis.__piWorkflowMonitor.archiveTimer);
|
|
186
|
+
globalThis.__piWorkflowMonitor.archiveTimer = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
resetRefs();
|
|
190
|
+
_guardrailsRef?.resetTracking();
|
|
191
|
+
_phaseReadyRef?.resetTracking();
|
|
192
|
+
clearActiveFeatureEnv();
|
|
193
|
+
clearFeatureStateCache();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Closure-wrapping functions ---
|
|
197
|
+
|
|
198
|
+
/** Substitute template variables in text using handler and empty-loops state */
|
|
199
|
+
export function substituteTemplates(
|
|
200
|
+
text: string,
|
|
201
|
+
featureStateOverride: FeatureState | null,
|
|
202
|
+
agentName: string | null,
|
|
203
|
+
): SubstitutionResult {
|
|
204
|
+
return skillExpansion.substituteTemplates(
|
|
205
|
+
text,
|
|
206
|
+
_handlerRef,
|
|
207
|
+
_emptyLoops?.getEmptyLoopsForSlug ?? null,
|
|
208
|
+
featureStateOverride,
|
|
209
|
+
_getAutoAgentCallbackRef ?? null,
|
|
210
|
+
agentName,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Expand skill command in text using handler and empty-loops state */
|
|
215
|
+
export function expandSkillCommand(
|
|
216
|
+
text: string,
|
|
217
|
+
featureStateOverride: FeatureState | null,
|
|
218
|
+
agentName: string | null,
|
|
219
|
+
): string {
|
|
220
|
+
return skillExpansion.expandSkillCommand(
|
|
221
|
+
text,
|
|
222
|
+
_handlerRef,
|
|
223
|
+
_emptyLoops?.getEmptyLoopsForSlug ?? null,
|
|
224
|
+
featureStateOverride,
|
|
225
|
+
_getAutoAgentCallbackRef ?? null,
|
|
226
|
+
agentName,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Get the review loop count for a given phase from feature state */
|
|
231
|
+
export function resolveLoopIndex(stage: string): number {
|
|
232
|
+
return getLoopCountForPhase(_handlerRef?.getActiveFeatureState() ?? null, stage);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Apply model override for a given phase, resolving review loop index automatically */
|
|
236
|
+
export async function applyModelOverrideForPhase(
|
|
237
|
+
pi: ExtensionAPI,
|
|
238
|
+
ctx: ExtensionContext,
|
|
239
|
+
stage: string,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
await applyModelOverride(pi, ctx, stage, resolveLoopIndex(stage));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Reset module-level refs (called by _resetFeatureState and tests) */
|
|
245
|
+
export function resetRefs(): void {
|
|
246
|
+
_handlerRef = null;
|
|
247
|
+
_emptyLoops = null;
|
|
248
|
+
_skillResolution = null;
|
|
249
|
+
_guardrailsRef = null;
|
|
250
|
+
_phaseReadyRef = null;
|
|
251
|
+
_getAutoAgentCallbackRef = null;
|
|
252
|
+
_agentJustFinishedRef.value = false;
|
|
253
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DI interfaces for workflow-monitor module decomposition.
|
|
6
|
+
*
|
|
7
|
+
* Each module declares its cross-module dependencies as interfaces.
|
|
8
|
+
* The orchestrator creates modules in topological dependency order and injects them.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext, ToolCallEvent, ToolResultEvent } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { getSettings } from "../settings/settings-ui.js";
|
|
13
|
+
import type { FeatureSession } from "../state/feature-session.js";
|
|
14
|
+
|
|
15
|
+
/** Shared context passed to all modules. */
|
|
16
|
+
export interface ModuleContext {
|
|
17
|
+
pi: ExtensionAPI;
|
|
18
|
+
handler: FeatureSession;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Common phase-transition dependencies shared by PhaseReadyDeps and WorkflowCommandDeps (pi + handler + model override + review→UAT transition). */
|
|
22
|
+
export interface WorkflowTransitionDeps {
|
|
23
|
+
pi: ExtensionAPI;
|
|
24
|
+
handler: FeatureSession;
|
|
25
|
+
applyModelOverrideForPhase: (pi: ExtensionAPI, ctx: ExtensionContext, stage: string) => Promise<void>;
|
|
26
|
+
handleReviewToUatTransition: (
|
|
27
|
+
ctx: ExtensionContext,
|
|
28
|
+
slug: string,
|
|
29
|
+
settings: ReturnType<typeof getSettings>,
|
|
30
|
+
// Review-report context for the worth-notes merge. Non-null on the after-review path
|
|
31
|
+
// so the report + handoff + worth-notes pointer merge into ONE notify; null at the
|
|
32
|
+
// off/after-finish paths and non-loop call sites (no merge). Required (no-optional-params).
|
|
33
|
+
reportCtx: { report: string | null; level: "info" | "warning"; pointer: string | null } | null,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compaction module — tracks empty loops, agent-finished state, and reviewer skip logic.
|
|
39
|
+
* Written to by Guardrails (incrementEmptyLoop), PhaseReady (resetEmptyLoop, isReviewerSkipped),
|
|
40
|
+
* and AgentLifecycle (setAgentFinished). Read by SessionLifecycle (resetTracking).
|
|
41
|
+
*/
|
|
42
|
+
export interface ICompaction {
|
|
43
|
+
setAgentFinished(value: boolean): void;
|
|
44
|
+
incrementEmptyLoop(slug: string, reviewer: string): void;
|
|
45
|
+
resetEmptyLoop(slug: string, reviewer: string): void;
|
|
46
|
+
isReviewerSkipped(slug: string, reviewer: string, threshold: number): boolean;
|
|
47
|
+
resetTracking(): void;
|
|
48
|
+
/** Get empty loops for a specific slug (used by substituteTemplates) */
|
|
49
|
+
getEmptyLoopsForSlug(slug: string): Record<string, number>;
|
|
50
|
+
/** Reset all empty loops across all slugs */
|
|
51
|
+
resetAllEmptyLoops(): void;
|
|
52
|
+
/** Get all reviewer empty loops (used by tests and debug) */
|
|
53
|
+
getReviewerEmptyLoops(): Record<string, Record<string, number>>;
|
|
54
|
+
/** Resolve expected skill for current workflow phase */
|
|
55
|
+
getExpectedSkill(): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* Recover from a FAILED compact: the agent's turn was aborted by ctx.compact(), so
|
|
58
|
+
* deliver the full follow-up resume (reusing the session_compact assembly — new-phase
|
|
59
|
+
* skill / task confirmation / todo details) + clear the re-entrancy guard. Injected into
|
|
60
|
+
* triggerContextCompact callers as the ctx.compact({onError}) handler.
|
|
61
|
+
*/
|
|
62
|
+
recoverCompactFailure(): void;
|
|
63
|
+
/**
|
|
64
|
+
* session_compact handler body: clear any pending deferred follow-up, then deliver
|
|
65
|
+
* the stored follow-up for the compaction reason (or delegate to the subagent
|
|
66
|
+
* compact path). Called by events/session/session-compact.ts.
|
|
67
|
+
*/
|
|
68
|
+
onSessionCompact(event: { reason: "manual" | "threshold" | "overflow" }): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* session_shutdown handler body: clear any pending deferred follow-up so a deferred
|
|
71
|
+
* inject never fires into a dead session. Called by events/session/session-shutdown.ts.
|
|
72
|
+
*/
|
|
73
|
+
onSessionShutdown(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Guardrails module — handles tool_call and tool_result guardrail checks,
|
|
78
|
+
* tracks verify-tests-passed state, pending violations, and branch tracking.
|
|
79
|
+
* Consumed by PhaseReady, SessionLifecycle, and the orchestrator.
|
|
80
|
+
*/
|
|
81
|
+
/** Per-tool tool_result advisory: warnings to prepend to the tool's output
|
|
82
|
+
* (TDD/process/pre-commit violations). */
|
|
83
|
+
export interface ToolResultAdvisory {
|
|
84
|
+
warnings: string[];
|
|
85
|
+
/** Workflow state changed — the router persists + refreshes the widget. */
|
|
86
|
+
changed?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Structural shape of a bash tool_result event (input.command + details.exitCode).
|
|
90
|
+
* The SDK exports only the ToolResultEvent union, not the bash-specific member. */
|
|
91
|
+
export interface BashResultEvent {
|
|
92
|
+
input: Record<string, unknown>;
|
|
93
|
+
content: ReadonlyArray<{ type: string; text?: string }>;
|
|
94
|
+
details: { exitCode?: number } | unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Per-tool tool_call decision: block the call with a reason, or allow; optionally
|
|
98
|
+
* signal that workflow state changed (router persists + refreshes the widget). */
|
|
99
|
+
export interface ToolCallDecision {
|
|
100
|
+
/** Block the tool call with this reason; undefined/null to allow. */
|
|
101
|
+
block?: string;
|
|
102
|
+
/** Workflow state changed — the router persists + refreshes the widget. */
|
|
103
|
+
changed?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface IGuardrails {
|
|
107
|
+
/** bash tool_call: fy-force-add block, publish gate, pre-commit discipline. */
|
|
108
|
+
onBashCall(event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallDecision>;
|
|
109
|
+
/** write/edit tool_call: TDD check, verify flag reset, phase-write-restriction, doc activation. */
|
|
110
|
+
onWriteEditCall(event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallDecision>;
|
|
111
|
+
/** phase_ready tool_call: subagent + implement-phase gating. */
|
|
112
|
+
onPhaseReadyCall(event: ToolCallEvent): ToolCallDecision;
|
|
113
|
+
/** task_ready_advance tool_call: subagent gating. */
|
|
114
|
+
onTaskReadyAdvanceCall(event: ToolCallEvent): ToolCallDecision;
|
|
115
|
+
/** read tool_result: log + record investigation. */
|
|
116
|
+
onReadResult(event: ToolResultEvent): ToolResultAdvisory;
|
|
117
|
+
/** write/edit tool_result: pending TDD violations + process warnings. */
|
|
118
|
+
onWriteEditResult(toolCallId: string): ToolResultAdvisory;
|
|
119
|
+
/** bash tool_result: record test outcome + verify flag, pending pre-commit warning. */
|
|
120
|
+
onBashResult(event: BashResultEvent, toolCallId: string): ToolResultAdvisory;
|
|
121
|
+
setVerifyTestsPassed(passed: boolean): void;
|
|
122
|
+
isVerifyTestsPassed(): boolean;
|
|
123
|
+
resetTracking(): void;
|
|
124
|
+
/** Complete a code-review loop from a phase_ready({issuesFound, cannotFix}) call:
|
|
125
|
+
* record review history, track reviewer empty loops, then drive the loop
|
|
126
|
+
* decision + (UAT/finish) transition. The counts come from the fy-review
|
|
127
|
+
* skill. */
|
|
128
|
+
completeCodeReviewLoop(
|
|
129
|
+
ctx: ExtensionContext,
|
|
130
|
+
issuesFound: number,
|
|
131
|
+
cannotFixIssues: number,
|
|
132
|
+
falsePositives: number,
|
|
133
|
+
): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* PhaseReady module — phase_ready tool handler.
|
|
138
|
+
* Design doc specifies this interface for DI consistency.
|
|
139
|
+
* Called by SessionLifecycle (resetTracking on fy:reset).
|
|
140
|
+
*/
|
|
141
|
+
export interface IPhaseReady {
|
|
142
|
+
resetTracking(): void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* SessionLifecycle module — the bodies of the session_start (state branch) and
|
|
147
|
+
* session_tree handlers as callable methods, plus the shared workflow reset.
|
|
148
|
+
* Pure domain logic (no pi.on registration); the events/session/ routers call these.
|
|
149
|
+
*/
|
|
150
|
+
export interface ISessionLifecycle {
|
|
151
|
+
/** session_start handler body (state branch). */
|
|
152
|
+
onSessionStart(event: unknown, ctx: ExtensionContext): Promise<void>;
|
|
153
|
+
/** session_tree handler body. */
|
|
154
|
+
onSessionTree(ctx: ExtensionContext): Promise<void>;
|
|
155
|
+
/** Shared reset logic used by /fy:reset, /resume session_start, and user-initiated /new. */
|
|
156
|
+
performWorkflowReset(): void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Re-export ViolationType from the closure scope — used by escalation tracking. */
|
|
160
|
+
export type ViolationType = "phase-write-restriction" | "tdd-write-order";
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* AgentLifecycle module — the bodies of the agent_start and agent_end handlers as
|
|
164
|
+
* callable methods. agent_start re-arms the finish guardrail + signals a fresh
|
|
165
|
+
* agent turn; agent_end clears the guardrail, resets the phase_ready guard, flags
|
|
166
|
+
* the agent finished (for compaction), and notifies the auto-agent on non-retryable
|
|
167
|
+
* execution-phase errors. Pure domain logic (no pi.on registration); the
|
|
168
|
+
* events/agent/ routers call these.
|
|
169
|
+
*/
|
|
170
|
+
export interface IAgentLifecycle {
|
|
171
|
+
/** agent_start handler body. */
|
|
172
|
+
onAgentStart(): Promise<void>;
|
|
173
|
+
/** agent_end handler body. */
|
|
174
|
+
onAgentEnd(
|
|
175
|
+
event: { messages?: ReadonlyArray<{ role: string; stopReason?: string; errorMessage?: string }> },
|
|
176
|
+
ctx: ExtensionContext,
|
|
177
|
+
): Promise<void>;
|
|
178
|
+
/** agent_settled handler body — pi will not continue automatically. */
|
|
179
|
+
onAgentSettled(): Promise<void>;
|
|
180
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dialog coordinator forwarding — drop-in integration with avtc-pi-ui-components DialogCoordinator.
|
|
6
|
+
*
|
|
7
|
+
* Copy this file into your extension's src/snippets/vendored/ directory, then:
|
|
8
|
+
* 1. Call subscribeToDialogCoordinator(pi) in your extension entry point
|
|
9
|
+
* 2. Call withCoordinator(fn) around any blocking ctx.ui.* call
|
|
10
|
+
*
|
|
11
|
+
* If avtc-pi-ui-components is not installed or the coordinator event never fires,
|
|
12
|
+
* all functions pass through (no-op).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared VALUE, stored on globalThis so it survives jiti `moduleCache: false` re-imports
|
|
19
|
+
* (which create distinct module instances and would otherwise split a module-level
|
|
20
|
+
* `let` between the setter in subscribeToDialogCoordinator() and the getter in
|
|
21
|
+
* withCoordinator()).
|
|
22
|
+
*
|
|
23
|
+
* `coordinator` is idempotent across subscribers — there is one DialogCoordinator,
|
|
24
|
+
* so one object. Listener unsubs are NOT shared: each subscribeToDialogCoordinator()
|
|
25
|
+
* call owns its own local closure unsubs.
|
|
26
|
+
*/
|
|
27
|
+
interface DialogCoordinatorForwardingState {
|
|
28
|
+
coordinator: { enqueueOrShow<T>(fn: () => Promise<T>): Promise<T> } | null;
|
|
29
|
+
}
|
|
30
|
+
const STATE_KEY = "__piDialogCoordinatorForwarding";
|
|
31
|
+
type GlobalWithState = typeof globalThis & { [STATE_KEY]?: DialogCoordinatorForwardingState };
|
|
32
|
+
function _state(): DialogCoordinatorForwardingState {
|
|
33
|
+
const g = globalThis as GlobalWithState;
|
|
34
|
+
if (!g[STATE_KEY]) {
|
|
35
|
+
g[STATE_KEY] = { coordinator: null };
|
|
36
|
+
}
|
|
37
|
+
const state = g[STATE_KEY];
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reset module state — called during test cleanup.
|
|
43
|
+
*/
|
|
44
|
+
export function _resetState(): void {
|
|
45
|
+
_state().coordinator = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to dialog-coordinator:ready event emitted by avtc-pi-ui-components.
|
|
50
|
+
* Call once per extension entry point.
|
|
51
|
+
*
|
|
52
|
+
* Reload-safe: session_shutdown fires before reload, cleaning all listeners.
|
|
53
|
+
*/
|
|
54
|
+
export function subscribeToDialogCoordinator(pi: ExtensionAPI): void {
|
|
55
|
+
if (!pi.events) return; // graceful no-op for incomplete test mocks
|
|
56
|
+
|
|
57
|
+
const unsubs: Array<() => void> = [];
|
|
58
|
+
|
|
59
|
+
// Listen for dialog-coordinator:ready (emitted by avtc-pi-ui-components in session_start)
|
|
60
|
+
unsubs.push(
|
|
61
|
+
pi.events.on("dialog-coordinator:ready", (data: unknown) => {
|
|
62
|
+
const api = data as {
|
|
63
|
+
coordinator?: { enqueueOrShow<T>(fn: () => Promise<T>): Promise<T> };
|
|
64
|
+
};
|
|
65
|
+
if (api?.coordinator && typeof api.coordinator.enqueueOrShow === "function") {
|
|
66
|
+
_state().coordinator = api.coordinator;
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Reset on session shutdown (fires before reload) — clean ONLY this subscriber's
|
|
72
|
+
// EventBus listener (pi.on listeners are torn down by pi), then clear shared value
|
|
73
|
+
// (hygiene; provider re-emits on next session_start).
|
|
74
|
+
pi.on("session_shutdown", () => {
|
|
75
|
+
for (const unsub of unsubs) {
|
|
76
|
+
if (typeof unsub === "function") unsub();
|
|
77
|
+
}
|
|
78
|
+
unsubs.length = 0;
|
|
79
|
+
_state().coordinator = null;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wrap an async UI call with dialog coordination.
|
|
85
|
+
* The call is serialized through the shared queue: if another dialog is currently
|
|
86
|
+
* showing, this call waits and is shown only after the active dialog resolves.
|
|
87
|
+
* If no coordinator is installed, the call is executed immediately (no-op).
|
|
88
|
+
*
|
|
89
|
+
* @param fn - Async function that blocks on user input (e.g. ctx.ui.select, ctx.ui.custom)
|
|
90
|
+
* @returns A promise that resolves with whatever `fn` returns.
|
|
91
|
+
*/
|
|
92
|
+
export async function withCoordinator<T>(fn: () => Promise<T>): Promise<T> {
|
|
93
|
+
const coordinator = _state().coordinator;
|
|
94
|
+
if (coordinator) {
|
|
95
|
+
return coordinator.enqueueOrShow(fn);
|
|
96
|
+
}
|
|
97
|
+
return fn();
|
|
98
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 avtc <tarasenkov@gmail.com>
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Notification forwarding — drop-in integration with pi-notification.
|
|
6
|
+
*
|
|
7
|
+
* Copy this file into your extension's src/snippets/vendored/ directory, then:
|
|
8
|
+
* 1. Call subscribeToNotificationApi(pi) in your extension entry point
|
|
9
|
+
* 2. Call requestAttention(source, detail) before any blocking UI call
|
|
10
|
+
* 3. Call the returned cancel function when unblocked
|
|
11
|
+
*
|
|
12
|
+
* If pi-notification is not installed, all functions are no-ops.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared VALUES, stored on globalThis so they survive jiti `moduleCache: false` re-imports
|
|
19
|
+
* (which create distinct module instances and would otherwise split module-level `let`
|
|
20
|
+
* variables between the setter in extension.ts and the getter in select-with-note.ts).
|
|
21
|
+
*
|
|
22
|
+
* Only idempotent VALUES live here — there is one provider (so one requestAttention fn)
|
|
23
|
+
* and one active session (so one lastMessage), observed identically by every
|
|
24
|
+
* subscriber. Listener unsubs are NOT shared: each subscribeToNotificationApi() call
|
|
25
|
+
* owns its own local closure unsubs, so multiple subscribers (ui-components,
|
|
26
|
+
* ask-user-question, featyard) don't clobber each other's cleanup.
|
|
27
|
+
*/
|
|
28
|
+
interface NotificationForwardingState {
|
|
29
|
+
requestAttention: ((source: string, detail?: string) => (() => void) | undefined) | null;
|
|
30
|
+
lastMessage: string;
|
|
31
|
+
}
|
|
32
|
+
const STATE_KEY = "__piNotificationForwarding";
|
|
33
|
+
type GlobalWithState = typeof globalThis & { [STATE_KEY]?: NotificationForwardingState };
|
|
34
|
+
function _state(): NotificationForwardingState {
|
|
35
|
+
const g = globalThis as GlobalWithState;
|
|
36
|
+
if (!g[STATE_KEY]) {
|
|
37
|
+
g[STATE_KEY] = { requestAttention: null, lastMessage: "" };
|
|
38
|
+
}
|
|
39
|
+
const state = g[STATE_KEY];
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract the last assistant text from a message_end event.
|
|
45
|
+
* Handles string content and array content (filters to text blocks, takes last).
|
|
46
|
+
* Returns empty string if no suitable text found.
|
|
47
|
+
*/
|
|
48
|
+
type MessageContentBlock = { type: string; text?: string };
|
|
49
|
+
|
|
50
|
+
export function extractLastAssistantText(event: {
|
|
51
|
+
message?: { role?: string; content?: string | MessageContentBlock[] };
|
|
52
|
+
}): string {
|
|
53
|
+
if (event.message?.role !== "assistant") return "";
|
|
54
|
+
|
|
55
|
+
const content = event.message.content;
|
|
56
|
+
if (typeof content === "string") return content;
|
|
57
|
+
if (Array.isArray(content)) {
|
|
58
|
+
const textBlocks = content.filter((b): b is MessageContentBlock & { type: "text" } => b.type === "text");
|
|
59
|
+
const last = textBlocks[textBlocks.length - 1];
|
|
60
|
+
if (last) {
|
|
61
|
+
return last.text ?? "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the last captured assistant message text.
|
|
69
|
+
* Returns empty string if no message has been captured yet.
|
|
70
|
+
*/
|
|
71
|
+
export function getLastMessage(): string {
|
|
72
|
+
return _state().lastMessage;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reset module state — called on session shutdown and during test cleanup.
|
|
77
|
+
* Clears shared values; listener cleanup is each subscriber's own responsibility
|
|
78
|
+
* (via its session_shutdown handler).
|
|
79
|
+
*/
|
|
80
|
+
export function _resetNotificationState(): void {
|
|
81
|
+
const s = _state();
|
|
82
|
+
s.lastMessage = "";
|
|
83
|
+
s.requestAttention = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Subscribe to pi-notification:ready event and message_end events.
|
|
88
|
+
* Call once per extension entry point (all will set the same shared reference).
|
|
89
|
+
*
|
|
90
|
+
* Reload-safe: session_shutdown fires before reload, cleaning all listeners.
|
|
91
|
+
*/
|
|
92
|
+
export function subscribeToNotificationApi(pi: ExtensionAPI): void {
|
|
93
|
+
const unsubs: Array<() => void> = [];
|
|
94
|
+
|
|
95
|
+
const unsub = pi.events.on("pi-notification:ready", (data: unknown) => {
|
|
96
|
+
const api = data as {
|
|
97
|
+
requestAttention?: (source: string, detail?: string) => (() => void) | undefined;
|
|
98
|
+
};
|
|
99
|
+
if (typeof api.requestAttention === "function" && api.requestAttention) {
|
|
100
|
+
_state().requestAttention = api.requestAttention;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
unsubs.push(unsub);
|
|
104
|
+
|
|
105
|
+
// Capture last assistant message for notification context.
|
|
106
|
+
// Note: pi.on (extension lifecycle events) returns void — these listeners are torn down
|
|
107
|
+
// by pi when it discards the extension runner on reload/session-end, so no manual unsub.
|
|
108
|
+
if (typeof pi.on === "function") {
|
|
109
|
+
pi.on("message_end", (event: { message?: { role?: string; content?: string | MessageContentBlock[] } }) => {
|
|
110
|
+
const text = extractLastAssistantText(event);
|
|
111
|
+
if (text) _state().lastMessage = text;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reset on session shutdown (fires before reload) — clean ONLY this subscriber's
|
|
116
|
+
// EventBus listener (pi.on listeners are torn down by pi), then clear shared values
|
|
117
|
+
// (hygiene; provider re-emits on next session_start).
|
|
118
|
+
pi.on("session_shutdown", () => {
|
|
119
|
+
for (const unsub of unsubs) unsub();
|
|
120
|
+
unsubs.length = 0;
|
|
121
|
+
const s = _state();
|
|
122
|
+
s.lastMessage = "";
|
|
123
|
+
s.requestAttention = null;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Request attention before blocking on user input.
|
|
129
|
+
* Returns a cancel function, or undefined if pi-notification is not installed.
|
|
130
|
+
*/
|
|
131
|
+
export function requestAttention(source: string, detail: string): (() => void) | undefined {
|
|
132
|
+
return _state().requestAttention?.(source, detail);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Wrap an async UI call with requestAttention/cancelAttention.
|
|
137
|
+
* If pi-notification is not installed, just calls the function directly.
|
|
138
|
+
*
|
|
139
|
+
* @param source - Who is requesting attention (e.g. "permission", "ask_user_question")
|
|
140
|
+
* @param detail - Optional detail (e.g. tool name, question summary)
|
|
141
|
+
* @param fn - Async function that blocks on user input
|
|
142
|
+
*/
|
|
143
|
+
export async function withAttention<T>(source: string, detail: string, fn: () => Promise<T>): Promise<T> {
|
|
144
|
+
const cancelAttention = requestAttention(source, detail);
|
|
145
|
+
try {
|
|
146
|
+
return await fn();
|
|
147
|
+
} finally {
|
|
148
|
+
cancelAttention?.();
|
|
149
|
+
}
|
|
150
|
+
}
|