@mrclrchtr/supi-consulting 0.0.0-stage → 9.0.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/CLAUDE.md +23 -0
- package/CONTEXT.md +53 -0
- package/README.md +104 -2
- package/docs/adr/0001-use-an-isolated-antigravity-home.md +5 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +137 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +73 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +42 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +342 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +17 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +313 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +64 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +38 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +356 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +242 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +47 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +454 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-core/src/tui.ts +141 -0
- package/package.json +73 -4
- package/scripts/live-probe.ts +139 -0
- package/src/agents/antigravity/activity.ts +22 -0
- package/src/agents/antigravity/adapter.ts +196 -0
- package/src/agents/antigravity/availability.ts +231 -0
- package/src/agents/antigravity/isolated-home.ts +346 -0
- package/src/agents/antigravity/models.ts +15 -0
- package/src/agents/antigravity/process/environment.ts +33 -0
- package/src/agents/antigravity/process/event-values.ts +273 -0
- package/src/agents/antigravity/process/events.ts +366 -0
- package/src/agents/antigravity/process/hooks.ts +219 -0
- package/src/agents/antigravity/process/ndjson.ts +120 -0
- package/src/agents/antigravity/process/protocol.ts +69 -0
- package/src/agents/antigravity/process/runner.ts +147 -0
- package/src/agents/antigravity/process/subprocess.ts +278 -0
- package/src/agents/antigravity/process/usage.ts +50 -0
- package/src/agents/antigravity/structured-output.ts +58 -0
- package/src/agents/antigravity/types.ts +17 -0
- package/src/agents/errors.ts +27 -0
- package/src/agents/types.ts +43 -0
- package/src/catalogue.ts +17 -0
- package/src/config.ts +26 -0
- package/src/conversation/handles.ts +187 -0
- package/src/evidence-hash.ts +6 -0
- package/src/extension.ts +26 -0
- package/src/footer-constants.ts +5 -0
- package/src/footer.ts +25 -0
- package/src/runtime.ts +244 -0
- package/src/settings.ts +38 -0
- package/src/tool/consulting_run/evidence.ts +169 -0
- package/src/tool/consulting_run/execute.ts +201 -0
- package/src/tool/consulting_run/guidance.ts +3 -0
- package/src/tool/consulting_run/input.ts +117 -0
- package/src/tool/consulting_run/register.ts +36 -0
- package/src/tool/consulting_run/render.ts +261 -0
- package/src/tool/consulting_run/result.ts +185 -0
- package/src/tool/consulting_run/spec.ts +20 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ConversationHandleRecord } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
/** Private custom-entry type used to rebuild handles on the active PI branch. */
|
|
6
|
+
export const CONSULTING_HANDLE_ENTRY_TYPE = "supi-consulting-handle";
|
|
7
|
+
|
|
8
|
+
interface PersistedHandleEntry {
|
|
9
|
+
version: 1;
|
|
10
|
+
action: "created" | "retired";
|
|
11
|
+
record?: ConversationHandleRecord;
|
|
12
|
+
handle?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Preflight failure for a Conversation Handle request. */
|
|
17
|
+
export class ConversationHandleError extends Error {
|
|
18
|
+
readonly code: "unknown" | "retired" | "busy" | "unsupported-agent";
|
|
19
|
+
|
|
20
|
+
constructor(code: ConversationHandleError["code"]) {
|
|
21
|
+
const messages = {
|
|
22
|
+
unknown: "Unknown Conversation Handle.",
|
|
23
|
+
retired: "That Conversation Handle is retired.",
|
|
24
|
+
busy: "That Conversation Handle is already in use.",
|
|
25
|
+
"unsupported-agent": "That Conversation Handle belongs to an unsupported Consulting Agent.",
|
|
26
|
+
};
|
|
27
|
+
super(messages[code]);
|
|
28
|
+
this.name = "ConversationHandleError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Session-local Conversation Handle state with branch-aware reconstruction. */
|
|
34
|
+
export class ConversationHandleStore {
|
|
35
|
+
#records = new Map<string, ConversationHandleRecord>();
|
|
36
|
+
#busy = new Set<string>();
|
|
37
|
+
|
|
38
|
+
/** Rebuild valid handles from one current PI branch. */
|
|
39
|
+
rebuild(branch: readonly unknown[]): void {
|
|
40
|
+
this.#records.clear();
|
|
41
|
+
this.#busy.clear();
|
|
42
|
+
for (const entry of branch) this.#consumeEntry(entry);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Create a new opaque Conversation Handle after a successful Consultation. */
|
|
46
|
+
create(record: Omit<ConversationHandleRecord, "handle" | "status">): ConversationHandleRecord {
|
|
47
|
+
const created: ConversationHandleRecord = Object.freeze({
|
|
48
|
+
...record,
|
|
49
|
+
handle: createOpaqueHandle(),
|
|
50
|
+
status: "active",
|
|
51
|
+
});
|
|
52
|
+
this.#records.set(created.handle, created);
|
|
53
|
+
return created;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Return a handle record without changing its state. */
|
|
57
|
+
get(handle: string): ConversationHandleRecord | undefined {
|
|
58
|
+
return this.#records.get(handle);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Acquire a handle after rejecting unknown, unsupported, retired, or busy state. */
|
|
62
|
+
acquire(handle: string, supportedAgent: string): ConversationHandleRecord {
|
|
63
|
+
const record = this.#records.get(handle);
|
|
64
|
+
if (!record) throw new ConversationHandleError("unknown");
|
|
65
|
+
if (record.agent !== supportedAgent) throw new ConversationHandleError("unsupported-agent");
|
|
66
|
+
if (record.status === "retired") throw new ConversationHandleError("retired");
|
|
67
|
+
if (this.#busy.has(handle)) throw new ConversationHandleError("busy");
|
|
68
|
+
this.#busy.add(handle);
|
|
69
|
+
return record;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Release a completed or pre-process follow-up. */
|
|
73
|
+
release(handle: string): void {
|
|
74
|
+
this.#busy.delete(handle);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Retire a handle after a started follow-up fails or is canceled. */
|
|
78
|
+
retire(handle: string): ConversationHandleRecord | undefined {
|
|
79
|
+
const record = this.#records.get(handle);
|
|
80
|
+
if (!record) return undefined;
|
|
81
|
+
const retired = Object.freeze({ ...record, status: "retired" as const });
|
|
82
|
+
this.#records.set(handle, retired);
|
|
83
|
+
this.#busy.delete(handle);
|
|
84
|
+
return retired;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Clear all state when the extension instance shuts down. */
|
|
88
|
+
clear(): void {
|
|
89
|
+
this.#records.clear();
|
|
90
|
+
this.#busy.clear();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#consumeEntry(entry: unknown): void {
|
|
94
|
+
if (!isRecord(entry)) return;
|
|
95
|
+
if (entry.type === "custom" && entry.customType === CONSULTING_HANDLE_ENTRY_TYPE) {
|
|
96
|
+
this.#consumePersistedData(entry.data);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (entry.type === "message" && isRecord(entry.message)) {
|
|
100
|
+
const message = entry.message;
|
|
101
|
+
if (message.role === "toolResult" && message.toolName === "consulting_run") {
|
|
102
|
+
this.#consumeResultDetails(message.details);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#consumePersistedData(value: unknown): void {
|
|
108
|
+
if (!isRecord(value) || value.version !== 1) return;
|
|
109
|
+
if (value.action === "created" && isHandleRecord(value.record)) {
|
|
110
|
+
this.#records.set(value.record.handle, Object.freeze({ ...value.record }));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (value.action === "retired" && typeof value.handle === "string") {
|
|
114
|
+
this.retire(value.handle);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#consumeResultDetails(value: unknown): void {
|
|
119
|
+
if (!isRecord(value)) return;
|
|
120
|
+
const record = {
|
|
121
|
+
handle: value.handle,
|
|
122
|
+
agent: value.agent,
|
|
123
|
+
model: value.model,
|
|
124
|
+
continuation: value.continuation,
|
|
125
|
+
canonicalWorkingDirectory: value.canonicalWorkingDirectory,
|
|
126
|
+
workspaceAccess: value.workspaceAccess,
|
|
127
|
+
agentVersion: value.agentVersion,
|
|
128
|
+
status: value.handleState,
|
|
129
|
+
};
|
|
130
|
+
if (isHandleRecord(record) && !this.#records.has(record.handle)) {
|
|
131
|
+
this.#records.set(record.handle, Object.freeze({ ...record }));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Persist a successful handle without adding model-visible content. */
|
|
137
|
+
export function appendHandleCreated(pi: ExtensionAPI, record: ConversationHandleRecord): void {
|
|
138
|
+
const data: PersistedHandleEntry = { version: 1, action: "created", record };
|
|
139
|
+
pi.appendEntry(CONSULTING_HANDLE_ENTRY_TYPE, data);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Persist a branch-aware retirement without exposing continuation state as model text. */
|
|
143
|
+
export function appendHandleRetired(
|
|
144
|
+
pi: ExtensionAPI,
|
|
145
|
+
record: ConversationHandleRecord,
|
|
146
|
+
reason: string,
|
|
147
|
+
): void {
|
|
148
|
+
const data: PersistedHandleEntry = {
|
|
149
|
+
version: 1,
|
|
150
|
+
action: "retired",
|
|
151
|
+
handle: record.handle,
|
|
152
|
+
reason: boundedReason(reason),
|
|
153
|
+
};
|
|
154
|
+
pi.appendEntry(CONSULTING_HANDLE_ENTRY_TYPE, data);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createOpaqueHandle(): string {
|
|
158
|
+
return `consult_${randomUUID().replaceAll("-", "")}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isHandleRecord(value: unknown): value is ConversationHandleRecord {
|
|
162
|
+
if (!isRecord(value)) return false;
|
|
163
|
+
return (
|
|
164
|
+
typeof value.handle === "string" &&
|
|
165
|
+
/^consult_[0-9a-f]+$/i.test(value.handle) &&
|
|
166
|
+
typeof value.agent === "string" &&
|
|
167
|
+
value.agent.length > 0 &&
|
|
168
|
+
typeof value.model === "string" &&
|
|
169
|
+
value.model.length > 0 &&
|
|
170
|
+
typeof value.continuation === "string" &&
|
|
171
|
+
value.continuation.length > 0 &&
|
|
172
|
+
value.continuation.length <= 1_000 &&
|
|
173
|
+
typeof value.canonicalWorkingDirectory === "string" &&
|
|
174
|
+
value.canonicalWorkingDirectory.length > 0 &&
|
|
175
|
+
typeof value.workspaceAccess === "boolean" &&
|
|
176
|
+
typeof value.agentVersion === "string" &&
|
|
177
|
+
(value.status === "active" || value.status === "retired")
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function boundedReason(value: string): string {
|
|
182
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 200) || "follow-up failed";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
186
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
187
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createAntigravityAdapter } from "./agents/antigravity/adapter.ts";
|
|
3
|
+
import { registerConsultingFooterContribution } from "./footer.ts";
|
|
4
|
+
import { ConsultingRuntime } from "./runtime.ts";
|
|
5
|
+
import { registerConsultingSettings } from "./settings.ts";
|
|
6
|
+
|
|
7
|
+
/** Register the opt-in Consulting extension with its Antigravity adapter. */
|
|
8
|
+
export default function consultingExtension(pi: ExtensionAPI): void {
|
|
9
|
+
const runtime = new ConsultingRuntime({ pi, adapter: createAntigravityAdapter() });
|
|
10
|
+
const footer = registerConsultingFooterContribution(runtime);
|
|
11
|
+
registerConsultingSettings(pi, runtime);
|
|
12
|
+
|
|
13
|
+
pi.on("session_start", (_event, ctx) => {
|
|
14
|
+
runtime.rebuildHandles(ctx.sessionManager.getBranch());
|
|
15
|
+
void runtime.startRefresh(ctx.cwd, ctx);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
19
|
+
runtime.rebuildHandles(ctx.sessionManager.getBranch());
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
pi.on("session_shutdown", async () => {
|
|
23
|
+
await runtime.shutdown();
|
|
24
|
+
footer.dispose();
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/footer.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { footerContributions } from "@mrclrchtr/supi-core/footer-registry";
|
|
2
|
+
import { CONSULTING_FOOTER_KEY } from "./footer-constants.ts";
|
|
3
|
+
import type { ConsultingRuntime } from "./runtime.ts";
|
|
4
|
+
|
|
5
|
+
/** Register the Consulting checking spinner and ready icon on the footer stats line. */
|
|
6
|
+
export function registerConsultingFooterContribution(runtime: ConsultingRuntime): {
|
|
7
|
+
dispose: () => void;
|
|
8
|
+
} {
|
|
9
|
+
footerContributions.register({
|
|
10
|
+
key: CONSULTING_FOOTER_KEY,
|
|
11
|
+
placement: "stats-end",
|
|
12
|
+
priority: 110,
|
|
13
|
+
render: () => {
|
|
14
|
+
const icon = runtime.footerIcon;
|
|
15
|
+
return icon ? `| ${icon}` : "";
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return { dispose: unregisterConsultingFooterContribution };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Remove the Consulting ready icon from the footer. */
|
|
23
|
+
export function unregisterConsultingFooterContribution(): void {
|
|
24
|
+
footerContributions.unregister(CONSULTING_FOOTER_KEY);
|
|
25
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { FOOTER_INVALIDATE_EVENT } from "@mrclrchtr/supi-core/footer-registry";
|
|
3
|
+
import { BRAILLE_SPINNER_FRAMES, SPINNER_INTERVAL_MS } from "@mrclrchtr/supi-core/spinner-frames";
|
|
4
|
+
import type {
|
|
5
|
+
ConsultingAgentAdapter,
|
|
6
|
+
ConsultingAgentAvailability,
|
|
7
|
+
ConsultingAvailability,
|
|
8
|
+
} from "./agents/types.ts";
|
|
9
|
+
import { loadConsultingConfig } from "./config.ts";
|
|
10
|
+
import { ConversationHandleStore } from "./conversation/handles.ts";
|
|
11
|
+
import { CONSULTING_FOOTER_KEY, CONSULTING_READY_ICON } from "./footer-constants.ts";
|
|
12
|
+
import { registerConsultingRunTool } from "./tool/consulting_run/register.ts";
|
|
13
|
+
import { CONSULTING_RUN_TOOL_NAME } from "./tool/consulting_run/spec.ts";
|
|
14
|
+
|
|
15
|
+
type FooterState = "idle" | "checking" | "ready";
|
|
16
|
+
|
|
17
|
+
/** Context needed to report availability and update the footer. */
|
|
18
|
+
export type ConsultingRefreshContext = {
|
|
19
|
+
ui: Pick<ExtensionContext["ui"], "notify" | "setStatus">;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Session runtime for immutable availability and Conversation Handle state. */
|
|
23
|
+
export class ConsultingRuntime {
|
|
24
|
+
readonly handles = new ConversationHandleStore();
|
|
25
|
+
readonly adapter: ConsultingAgentAdapter;
|
|
26
|
+
#pi: ExtensionAPI;
|
|
27
|
+
#homeDir: string | undefined;
|
|
28
|
+
#availability: ConsultingAgentAvailability | undefined;
|
|
29
|
+
#refreshGeneration = 0;
|
|
30
|
+
#refreshAbort: AbortController | undefined;
|
|
31
|
+
#statusUi: Pick<ExtensionContext["ui"], "setStatus"> | undefined;
|
|
32
|
+
#footerState: FooterState = "idle";
|
|
33
|
+
#spinnerTimer: ReturnType<typeof setInterval> | undefined;
|
|
34
|
+
#spinnerFrame = 0;
|
|
35
|
+
#toolRegistered = false;
|
|
36
|
+
|
|
37
|
+
constructor(options: { pi: ExtensionAPI; adapter: ConsultingAgentAdapter; homeDir?: string }) {
|
|
38
|
+
this.#pi = options.pi;
|
|
39
|
+
this.adapter = options.adapter;
|
|
40
|
+
this.#homeDir = options.homeDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The immutable discovery result, if session discovery has completed. */
|
|
44
|
+
get availability(): ConsultingAgentAvailability | undefined {
|
|
45
|
+
return this.#availability;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether the discovered Consultation tool is ready for use. */
|
|
49
|
+
get isReady(): boolean {
|
|
50
|
+
return this.#footerState === "ready";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Return the animated checking icon or the settled ready icon for the footer. */
|
|
54
|
+
get footerIcon(): string | undefined {
|
|
55
|
+
if (this.#footerState === "checking") {
|
|
56
|
+
return BRAILLE_SPINNER_FRAMES[this.#spinnerFrame % BRAILLE_SPINNER_FRAMES.length];
|
|
57
|
+
}
|
|
58
|
+
return this.#footerState === "ready" ? CONSULTING_READY_ICON : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Rebuild handles from the current PI branch. */
|
|
62
|
+
rebuildHandles(branch: readonly unknown[]): void {
|
|
63
|
+
this.handles.rebuild(branch);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Start availability discovery without making Pi session startup wait. */
|
|
67
|
+
startRefresh(cwd: string, context?: ConsultingRefreshContext): Promise<void> {
|
|
68
|
+
return this.refresh(cwd, context).catch((error) => {
|
|
69
|
+
try {
|
|
70
|
+
// biome-ignore lint/suspicious/noConsole: unexpected failures must stay visible.
|
|
71
|
+
console.warn(`[supi-consulting] Availability check failed: ${formatRefreshError(error)}`);
|
|
72
|
+
context?.ui.notify("Consulting availability check failed. Reload PI to retry.", "warning");
|
|
73
|
+
} catch {
|
|
74
|
+
// PI may be shutting down while the refresh completes.
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Discover or reuse availability and synchronize the active tool. */
|
|
80
|
+
async refresh(cwd: string, context?: ConsultingRefreshContext): Promise<void> {
|
|
81
|
+
const generation = ++this.#refreshGeneration;
|
|
82
|
+
this.#refreshAbort?.abort();
|
|
83
|
+
const abortController = new AbortController();
|
|
84
|
+
this.#refreshAbort = abortController;
|
|
85
|
+
if (context) this.#statusUi = context.ui;
|
|
86
|
+
this.#stopSpinner();
|
|
87
|
+
this.#setFooterState("idle");
|
|
88
|
+
const config = loadConsultingConfig(cwd, this.#homeDir);
|
|
89
|
+
if (!config.agentToolEnabled) {
|
|
90
|
+
this.#deactivateTool();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.#startSpinner();
|
|
94
|
+
|
|
95
|
+
let availability: ConsultingAvailability =
|
|
96
|
+
this.#availability ??
|
|
97
|
+
(await this.adapter.discover({ signal: abortController.signal }).catch(() => ({
|
|
98
|
+
status: "unavailable",
|
|
99
|
+
reason: "discovery",
|
|
100
|
+
warning:
|
|
101
|
+
"Consulting Agent availability discovery failed. Check the installation, then reload PI.",
|
|
102
|
+
})));
|
|
103
|
+
if (generation !== this.#refreshGeneration || abortController.signal.aborted) return;
|
|
104
|
+
if (availability.status === "available") {
|
|
105
|
+
try {
|
|
106
|
+
availability = freezeAvailability(availability, this.adapter.identity);
|
|
107
|
+
this.#availability = availability;
|
|
108
|
+
this.#activateTool(availability);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.#stopSpinner();
|
|
111
|
+
this.#setFooterState("idle");
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
this.#stopSpinner();
|
|
115
|
+
this.#setFooterState("ready");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.#stopSpinner();
|
|
119
|
+
this.#setFooterState("idle");
|
|
120
|
+
this.#deactivateTool();
|
|
121
|
+
notifyRefreshWarning(context, availability.warning);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Stop in-flight discovery and clear session-local state. */
|
|
125
|
+
async shutdown(): Promise<void> {
|
|
126
|
+
this.#refreshGeneration += 1;
|
|
127
|
+
this.#refreshAbort?.abort();
|
|
128
|
+
this.#refreshAbort = undefined;
|
|
129
|
+
this.#stopSpinner();
|
|
130
|
+
this.#setFooterState("idle");
|
|
131
|
+
this.#deactivateTool();
|
|
132
|
+
this.#statusUi = undefined;
|
|
133
|
+
this.handles.clear();
|
|
134
|
+
await Promise.resolve();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#activateTool(availability: ConsultingAgentAvailability): void {
|
|
138
|
+
if (!this.#toolRegistered) {
|
|
139
|
+
registerConsultingRunTool({
|
|
140
|
+
pi: this.#pi,
|
|
141
|
+
adapter: this.adapter,
|
|
142
|
+
availability,
|
|
143
|
+
handles: this.handles,
|
|
144
|
+
});
|
|
145
|
+
this.#toolRegistered = true;
|
|
146
|
+
}
|
|
147
|
+
const activeTools = this.#pi.getActiveTools();
|
|
148
|
+
if (!activeTools.includes(CONSULTING_RUN_TOOL_NAME)) {
|
|
149
|
+
this.#pi.setActiveTools([...activeTools, CONSULTING_RUN_TOOL_NAME]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#deactivateTool(): void {
|
|
154
|
+
const activeTools = this.#pi.getActiveTools();
|
|
155
|
+
if (activeTools.includes(CONSULTING_RUN_TOOL_NAME)) {
|
|
156
|
+
this.#pi.setActiveTools(activeTools.filter((name) => name !== CONSULTING_RUN_TOOL_NAME));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#startSpinner(): void {
|
|
161
|
+
this.#stopSpinner();
|
|
162
|
+
this.#spinnerFrame = 0;
|
|
163
|
+
this.#setFooterState("checking");
|
|
164
|
+
this.#spinnerTimer = setInterval(() => {
|
|
165
|
+
if (this.#footerState !== "checking") return;
|
|
166
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
|
|
167
|
+
this.#publishFooter();
|
|
168
|
+
this.#invalidateFooter();
|
|
169
|
+
}, SPINNER_INTERVAL_MS);
|
|
170
|
+
this.#spinnerTimer.unref?.();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#stopSpinner(): void {
|
|
174
|
+
if (this.#spinnerTimer === undefined) return;
|
|
175
|
+
clearInterval(this.#spinnerTimer);
|
|
176
|
+
this.#spinnerTimer = undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#setFooterState(state: FooterState): void {
|
|
180
|
+
const changed = this.#footerState !== state;
|
|
181
|
+
this.#footerState = state;
|
|
182
|
+
this.#publishFooter();
|
|
183
|
+
if (!changed) return;
|
|
184
|
+
this.#invalidateFooter();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#publishFooter(): void {
|
|
188
|
+
try {
|
|
189
|
+
this.#statusUi?.setStatus(CONSULTING_FOOTER_KEY, this.footerIcon);
|
|
190
|
+
} catch {
|
|
191
|
+
// PI may be shutting down while the footer status changes.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#invalidateFooter(): void {
|
|
196
|
+
try {
|
|
197
|
+
this.#pi.events.emit(FOOTER_INVALIDATE_EVENT, {});
|
|
198
|
+
} catch {
|
|
199
|
+
// Footer refresh is optional and must not change runtime state.
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function freezeAvailability(
|
|
205
|
+
availability: ConsultingAgentAvailability,
|
|
206
|
+
expectedAgent: string,
|
|
207
|
+
): ConsultingAgentAvailability {
|
|
208
|
+
if (
|
|
209
|
+
availability.agent !== expectedAgent ||
|
|
210
|
+
!availability.agentVersion ||
|
|
211
|
+
availability.catalogue.length === 0 ||
|
|
212
|
+
availability.catalogue.some((model) => typeof model !== "string" || model.length === 0)
|
|
213
|
+
) {
|
|
214
|
+
throw new Error("Consulting Agent returned an invalid availability snapshot.");
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
...availability,
|
|
218
|
+
catalogue: Object.freeze([...availability.catalogue]),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatRefreshError(error: unknown): string {
|
|
223
|
+
const message =
|
|
224
|
+
error instanceof Error
|
|
225
|
+
? error.message
|
|
226
|
+
: typeof error === "object" &&
|
|
227
|
+
error !== null &&
|
|
228
|
+
"message" in error &&
|
|
229
|
+
typeof error.message === "string"
|
|
230
|
+
? error.message
|
|
231
|
+
: String(error);
|
|
232
|
+
return message.replace(/\s+/g, " ").trim().slice(0, 200) || "unknown error";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function notifyRefreshWarning(
|
|
236
|
+
context: ConsultingRefreshContext | undefined,
|
|
237
|
+
message: string,
|
|
238
|
+
): void {
|
|
239
|
+
try {
|
|
240
|
+
context?.ui.notify(message, "warning");
|
|
241
|
+
} catch {
|
|
242
|
+
// PI may be shutting down while the refresh reports its result.
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { defineConfigSettings, registerSettings } from "@mrclrchtr/supi-core/settings";
|
|
3
|
+
import { CONSULTING_CONFIG_SECTION, CONSULTING_DEFAULTS } from "./config.ts";
|
|
4
|
+
import type { ConsultingRuntime } from "./runtime.ts";
|
|
5
|
+
|
|
6
|
+
/** Register the Consulting setting with an awaited availability refresh. */
|
|
7
|
+
export function registerConsultingSettings(
|
|
8
|
+
pi: ExtensionAPI,
|
|
9
|
+
runtime: ConsultingRuntime,
|
|
10
|
+
homeDir?: string,
|
|
11
|
+
): void {
|
|
12
|
+
const fixedSettings = defineConfigSettings({
|
|
13
|
+
id: CONSULTING_CONFIG_SECTION,
|
|
14
|
+
label: "Consulting",
|
|
15
|
+
section: CONSULTING_CONFIG_SECTION,
|
|
16
|
+
defaults: CONSULTING_DEFAULTS,
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
kind: "boolean" as const,
|
|
20
|
+
key: "agentToolEnabled",
|
|
21
|
+
label: "Consultation tool",
|
|
22
|
+
description: "Enable consulting_run and its availability checks.",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
...(homeDir ? { homeDir } : {}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerSettings(pi, {
|
|
29
|
+
...fixedSettings,
|
|
30
|
+
apply: async (request) => {
|
|
31
|
+
const result = await fixedSettings.apply(request);
|
|
32
|
+
if (request.fieldKey === "agentToolEnabled") {
|
|
33
|
+
await runtime.refresh(request.cwd, request.ctx ? { ui: request.ctx.ui } : undefined);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|