@timurproko/a1 0.1.8-dev.226 → 0.1.8-dev.259
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/bin/guardian.js +4 -2
- package/bin/supervisor.js +1 -1
- package/bin/ui.js +44 -36
- package/bin/warmup.js +18 -9
- package/dist/cli/dispatch.d.ts +9 -1
- package/dist/cli/dispatch.js +73 -14
- package/dist/cli/package-messages.d.ts +26 -0
- package/dist/cli/package-messages.js +71 -0
- package/dist/cli/packages.d.ts +2 -1
- package/dist/cli/packages.js +12 -19
- package/dist/composition/owned-ui.d.ts +5 -2
- package/dist/composition/owned-ui.js +3 -1
- package/dist/contracts/agent-engine/package-ports.d.ts +6 -0
- package/dist/features/launch/intent.d.ts +5 -3
- package/dist/features/launch/intent.js +7 -2
- package/dist/features/owned-ui/index.d.ts +1 -0
- package/dist/features/owned-ui/index.js +1 -0
- package/dist/features/owned-ui/session-fork-prompt.d.ts +8 -0
- package/dist/features/owned-ui/session-fork-prompt.js +34 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -0
- package/dist/foundation/launch-guardian/main.js +8 -3
- package/dist/foundation/lifecycle/index.d.ts +2 -0
- package/dist/foundation/lifecycle/index.js +2 -0
- package/dist/foundation/lifecycle/paths.d.ts +2 -0
- package/dist/foundation/lifecycle/paths.js +9 -2
- package/dist/foundation/lifecycle/session-selection.d.ts +9 -0
- package/dist/foundation/lifecycle/session-selection.js +39 -0
- package/dist/foundation/lifecycle/supervisor-startup.d.ts +30 -0
- package/dist/foundation/lifecycle/supervisor-startup.js +106 -0
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +5 -0
- package/dist/foundation/process-containment/darwin-process-inspector.js +7 -0
- package/dist/foundation/process-containment/index.d.ts +1 -0
- package/dist/foundation/process-containment/index.js +1 -0
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/windows-process-inspector.js +4 -2
- package/dist/foundation/release/bootstrap.d.ts +10 -3
- package/dist/foundation/release/bootstrap.js +58 -25
- package/dist/foundation/release/dependency-layer.js +11 -10
- package/dist/foundation/release/immutable-platform.d.ts +6 -0
- package/dist/foundation/release/immutable-platform.js +13 -0
- package/dist/foundation/release/index.d.ts +1 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/release-store.d.ts +1 -1
- package/dist/foundation/release/release-store.js +4 -2
- package/dist/foundation/release/restart-certification.d.ts +67 -0
- package/dist/foundation/release/restart-certification.js +211 -0
- package/dist/foundation/release/update.js +4 -4
- package/dist/foundation/release/warmup.js +1 -1
- package/dist/foundation/startup/startup-runtime.d.ts +2 -2
- package/dist/foundation/startup/startup-runtime.js +1 -1
- package/dist/foundation/supervision/main.d.ts +1 -1
- package/dist/foundation/supervision/main.js +60 -30
- package/dist/foundation/supervision/server.js +14 -1
- package/dist/integrations/pi/components/shell-footer-status.js +28 -12
- package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +4 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +5 -0
- package/dist/integrations/pi/engine/adapter.d.ts +6 -0
- package/dist/integrations/pi/engine/adapter.js +354 -53
- package/dist/integrations/pi/engine/index.d.ts +1 -0
- package/dist/integrations/pi/engine/index.js +1 -0
- package/dist/integrations/pi/engine/package-integration.js +31 -25
- package/dist/integrations/pi/engine/runtime-integration.d.ts +3 -0
- package/dist/integrations/pi/engine/runtime-integration.js +11 -4
- package/dist/integrations/pi/engine/session-selection.d.ts +28 -0
- package/dist/integrations/pi/engine/session-selection.js +90 -0
- package/dist/integrations/pi/engine/workflows.d.ts +18 -1
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +5 -1
- package/dist/integrations/pi/session-ui/session-shell-root.js +76 -24
- package/dist/integrations/pi/session-ui/session-shell.d.ts +1 -1
- package/dist/integrations/pi/session-ui/session-shell.js +46 -14
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +2 -1
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +47 -18
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +12 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +166 -26
- package/dist/native/darwin-arm64/manifest.json +4 -4
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/index.d.ts +1 -0
- package/dist/ui/components/index.js +1 -0
- package/dist/ui/components/text-selection.js +8 -35
- package/dist/ui/components/transcript-viewport.d.ts +8 -2
- package/dist/ui/components/transcript-viewport.js +8 -6
- package/dist/ui/components/visible-hyperlinks.d.ts +19 -0
- package/dist/ui/components/visible-hyperlinks.js +64 -0
- package/docs/architecture/process-guardian-provenance.md +3 -1
- package/docs/architecture/ui-reference-provenance.md +2 -0
- package/docs/ci-release-runbook.md +1 -1
- package/docs/features/launch-profiles.md +17 -1
- package/docs/manual-ghost-link-baseline.md +89 -0
- package/package.json +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { dirname, join, resolve } from "node:path";
|
|
6
7
|
import { promisify } from "node:util";
|
|
7
8
|
import { PRODUCT_IDENTITY } from "../../../product-identity.js";
|
|
8
9
|
import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
|
|
9
|
-
import { copyToClipboard, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { copyToClipboard, CredentialSynchronizationError, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiSnapshot, } from "../../../contracts/owned-ui/index.js";
|
|
11
12
|
import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
|
|
12
13
|
import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
@@ -23,6 +24,50 @@ const execFileAsync = promisify(execFile);
|
|
|
23
24
|
// input and makes an in-progress mouse selection appear frozen.
|
|
24
25
|
const EVENT_DELIVERY_BATCH = 1;
|
|
25
26
|
const TOOL_UPDATE_COALESCE_MS = 50;
|
|
27
|
+
const AUTH_REFRESH_TIMEOUT_MS = 15_000;
|
|
28
|
+
// Provenance: Pi 0.84.2 core/model-resolver.ts defaultModelPerProvider.
|
|
29
|
+
const PINNED_DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
|
|
30
|
+
"amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
|
|
31
|
+
"ant-ling": "Ring-2.6-1T",
|
|
32
|
+
anthropic: "claude-opus-4-8",
|
|
33
|
+
openai: "gpt-5.5",
|
|
34
|
+
"azure-openai-responses": "gpt-5.4",
|
|
35
|
+
"openai-codex": "gpt-5.5",
|
|
36
|
+
radius: "auto",
|
|
37
|
+
nvidia: "nvidia/nemotron-3-super-120b-a12b",
|
|
38
|
+
deepseek: "deepseek-v4-pro",
|
|
39
|
+
google: "gemini-3.1-pro-preview",
|
|
40
|
+
"google-vertex": "gemini-3.1-pro-preview",
|
|
41
|
+
"github-copilot": "gpt-5.4",
|
|
42
|
+
openrouter: "moonshotai/kimi-k2.6",
|
|
43
|
+
"vercel-ai-gateway": "zai/glm-5.1",
|
|
44
|
+
xai: "grok-4.5",
|
|
45
|
+
groq: "openai/gpt-oss-120b",
|
|
46
|
+
cerebras: "zai-glm-4.7",
|
|
47
|
+
zai: "glm-5.1",
|
|
48
|
+
"zai-coding-cn": "glm-5.1",
|
|
49
|
+
mistral: "devstral-medium-latest",
|
|
50
|
+
minimax: "MiniMax-M2.7",
|
|
51
|
+
"minimax-cn": "MiniMax-M2.7",
|
|
52
|
+
moonshotai: "kimi-k2.6",
|
|
53
|
+
"moonshotai-cn": "kimi-k2.6",
|
|
54
|
+
huggingface: "moonshotai/Kimi-K2.6",
|
|
55
|
+
fireworks: "accounts/fireworks/models/kimi-k2p6",
|
|
56
|
+
together: "moonshotai/Kimi-K2.6",
|
|
57
|
+
baseten: "zai-org/GLM-5.2",
|
|
58
|
+
opencode: "kimi-k2.6",
|
|
59
|
+
"opencode-go": "kimi-k2.6",
|
|
60
|
+
"kimi-coding": "kimi-for-coding",
|
|
61
|
+
"cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6",
|
|
62
|
+
"cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6",
|
|
63
|
+
"qwen-token-plan": "qwen3.7-max",
|
|
64
|
+
"qwen-token-plan-cn": "qwen3.7-max",
|
|
65
|
+
"qwen-token-plan-individual": "qwen3.8-max",
|
|
66
|
+
xiaomi: "mimo-v2.5-pro",
|
|
67
|
+
"xiaomi-token-plan-cn": "mimo-v2.5-pro",
|
|
68
|
+
"xiaomi-token-plan-ams": "mimo-v2.5-pro",
|
|
69
|
+
"xiaomi-token-plan-sgp": "mimo-v2.5-pro",
|
|
70
|
+
});
|
|
26
71
|
const DEFAULT_SURFACE = {
|
|
27
72
|
columns: 100,
|
|
28
73
|
rows: 32,
|
|
@@ -37,6 +82,8 @@ export class PiEngineAdapter {
|
|
|
37
82
|
#agentDir;
|
|
38
83
|
#sessionId;
|
|
39
84
|
#sessionPath;
|
|
85
|
+
#sessionSelection;
|
|
86
|
+
#sessionForkPrompt;
|
|
40
87
|
#workflowHost;
|
|
41
88
|
#workflowInteraction;
|
|
42
89
|
#listeners = new Set();
|
|
@@ -99,6 +146,8 @@ export class PiEngineAdapter {
|
|
|
99
146
|
this.#agentDir = options.agentDir ?? getAgentDir();
|
|
100
147
|
this.#sessionId = options.sessionId ?? "owned-session-1";
|
|
101
148
|
this.#sessionPath = options.sessionPath;
|
|
149
|
+
this.#sessionSelection = options.sessionSelection;
|
|
150
|
+
this.#sessionForkPrompt = options.sessionForkPrompt;
|
|
102
151
|
this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
|
|
103
152
|
this.#checkPackageUpdates = options.checkPackageUpdates
|
|
104
153
|
?? (options.createRuntime
|
|
@@ -119,6 +168,9 @@ export class PiEngineAdapter {
|
|
|
119
168
|
get sessionGeneration() {
|
|
120
169
|
return this.#sessionGeneration;
|
|
121
170
|
}
|
|
171
|
+
get cwd() {
|
|
172
|
+
return this.#runtime?.cwd ?? this.#cwd;
|
|
173
|
+
}
|
|
122
174
|
get agentDir() {
|
|
123
175
|
return this.#agentDir;
|
|
124
176
|
}
|
|
@@ -137,7 +189,7 @@ export class PiEngineAdapter {
|
|
|
137
189
|
|| typeof manager.getSessionDir !== "function"
|
|
138
190
|
|| typeof manager.usesDefaultSessionDir !== "function"
|
|
139
191
|
|| manager.isPersisted() !== true
|
|
140
|
-
|| this.currentSessionFile()
|
|
192
|
+
|| !existsSync(this.currentSessionFile() ?? ""))
|
|
141
193
|
return null;
|
|
142
194
|
const sessionId = manager.getSessionId();
|
|
143
195
|
const sessionDir = manager.getSessionDir();
|
|
@@ -156,6 +208,8 @@ export class PiEngineAdapter {
|
|
|
156
208
|
agentDir: this.#agentDir,
|
|
157
209
|
sessionId: this.#sessionId,
|
|
158
210
|
...(this.#sessionPath === undefined ? {} : { sessionPath: this.#sessionPath }),
|
|
211
|
+
...(this.#sessionSelection === undefined ? {} : { sessionSelection: this.#sessionSelection }),
|
|
212
|
+
...(this.#sessionForkPrompt === undefined ? {} : { sessionForkPrompt: this.#sessionForkPrompt }),
|
|
159
213
|
...(this.#projectTrustPrompt === undefined ? {} : { projectTrustPrompt: this.#projectTrustPrompt }),
|
|
160
214
|
}).catch(error => {
|
|
161
215
|
this.#lifecycle = "failed";
|
|
@@ -163,6 +217,7 @@ export class PiEngineAdapter {
|
|
|
163
217
|
throw error;
|
|
164
218
|
});
|
|
165
219
|
this.#runtime = runtime;
|
|
220
|
+
this.#cwd = runtime.cwd ?? this.#cwd;
|
|
166
221
|
this.#gitBranch = await readGitBranch(this.#cwd);
|
|
167
222
|
this.#terminal = {
|
|
168
223
|
...this.#terminal,
|
|
@@ -458,7 +513,8 @@ export class PiEngineAdapter {
|
|
|
458
513
|
const session = this.#requireWorkflowSession();
|
|
459
514
|
const result = await requireCapability(session.cycleModel, "cycleModel").call(session, direction);
|
|
460
515
|
if (!isRecord(result) || !isRecord(result.model)) {
|
|
461
|
-
|
|
516
|
+
const scoped = Array.isArray(session.scopedModels) && session.scopedModels.length > 0;
|
|
517
|
+
return workflowResult("model", "completed", scoped ? "Only one model in scope" : "Only one model available", undefined, "status");
|
|
462
518
|
}
|
|
463
519
|
const model = readModel(result.model);
|
|
464
520
|
if (model)
|
|
@@ -466,7 +522,11 @@ export class PiEngineAdapter {
|
|
|
466
522
|
if (result.thinkingLevel !== undefined)
|
|
467
523
|
this.#thinkingLevel = readThinkingLevel(result.thinkingLevel);
|
|
468
524
|
this.#emitView();
|
|
469
|
-
|
|
525
|
+
const modelName = stringProperty(result.model, "name") ?? stringProperty(result.model, "id") ?? "model";
|
|
526
|
+
const reasoning = result.model.reasoning === true;
|
|
527
|
+
const thinking = this.#thinkingLevel;
|
|
528
|
+
const suffix = reasoning && thinking !== "off" ? ` (thinking: ${thinking})` : "";
|
|
529
|
+
return workflowResult("model", "completed", `Switched to ${modelName}${suffix}`, undefined, "status");
|
|
470
530
|
}
|
|
471
531
|
catch (error) {
|
|
472
532
|
return workflowResult("model", "failed", error instanceof Error ? error.message : String(error));
|
|
@@ -875,7 +935,13 @@ export class PiEngineAdapter {
|
|
|
875
935
|
const message = error instanceof Error ? error.message : String(error);
|
|
876
936
|
const contextualMessage = request.command === "export"
|
|
877
937
|
? `Failed to export session: ${message}`
|
|
878
|
-
: request.command === "
|
|
938
|
+
: request.command === "import"
|
|
939
|
+
? `Failed to import session: ${message}`
|
|
940
|
+
: request.command === "new"
|
|
941
|
+
? `Failed to create session: ${message}`
|
|
942
|
+
: request.command === "resume"
|
|
943
|
+
? `Failed to resume session: ${message}`
|
|
944
|
+
: request.command === "reload" ? `Reload failed: ${message}` : message;
|
|
879
945
|
return workflowResult(request.command, "failed", contextualMessage);
|
|
880
946
|
}
|
|
881
947
|
}
|
|
@@ -994,16 +1060,60 @@ export class PiEngineAdapter {
|
|
|
994
1060
|
const reference = selection ?? argument;
|
|
995
1061
|
if (!reference)
|
|
996
1062
|
return workflowResult(request.command, "failed", "Model requires the owned model controller");
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1063
|
+
const scopedModels = Array.isArray(session.scopedModels)
|
|
1064
|
+
? session.scopedModels.map(item => item.model)
|
|
1065
|
+
: [];
|
|
1066
|
+
let availableModels = scopedModels.length > 0
|
|
1067
|
+
? scopedModels
|
|
1068
|
+
: [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
|
|
1069
|
+
let model = findExactWorkflowModel(reference, availableModels);
|
|
1070
|
+
const messages = [];
|
|
1071
|
+
if (model === undefined && scopedModels.length === 0) {
|
|
1072
|
+
messages.push({ kind: "status", message: "Refreshing model catalogs…" });
|
|
1073
|
+
const controller = new AbortController();
|
|
1074
|
+
let timedOut = false;
|
|
1075
|
+
const timeout = setTimeout(() => {
|
|
1076
|
+
timedOut = true;
|
|
1077
|
+
controller.abort();
|
|
1078
|
+
}, AUTH_REFRESH_TIMEOUT_MS);
|
|
1079
|
+
try {
|
|
1080
|
+
const refreshed = await runtime.services.modelRuntime.refresh?.({ signal: controller.signal });
|
|
1081
|
+
if (isRecord(refreshed) && refreshed.aborted === true && timedOut) {
|
|
1082
|
+
messages.push({ kind: "warning", message: "Model refresh timed out; searching cached models." });
|
|
1083
|
+
}
|
|
1084
|
+
else if (isRecord(refreshed) && refreshed.errors instanceof Map && refreshed.errors.size > 0) {
|
|
1085
|
+
messages.push({ kind: "warning", message: `Could not refresh ${[...refreshed.errors.keys()].join(", ")}; searching cached models.` });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
messages.push({
|
|
1090
|
+
kind: "warning",
|
|
1091
|
+
message: timedOut
|
|
1092
|
+
? "Model refresh timed out; searching cached models."
|
|
1093
|
+
: `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
finally {
|
|
1097
|
+
clearTimeout(timeout);
|
|
1098
|
+
}
|
|
1099
|
+
availableModels = [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
|
|
1100
|
+
model = findExactWorkflowModel(reference, availableModels);
|
|
1101
|
+
}
|
|
1102
|
+
if (model === undefined) {
|
|
1103
|
+
return {
|
|
1104
|
+
...workflowResult(request.command, "requires-selection", "Select a model", reference, "silent"),
|
|
1105
|
+
...(messages.length === 0 ? {} : { messages: Object.freeze(messages) }),
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1003
1108
|
await session.setModel(model);
|
|
1109
|
+
const providerId = stringProperty(model, "provider") ?? "unknown";
|
|
1110
|
+
const modelId = stringProperty(model, "id") ?? reference;
|
|
1004
1111
|
this.#activeModel = { providerId, modelId, displayName: stringProperty(model, "name") ?? modelId };
|
|
1005
1112
|
this.#emitView();
|
|
1006
|
-
|
|
1113
|
+
const resultMessage = { kind: "status", message: `Model: ${modelId}` };
|
|
1114
|
+
return messages.length === 0
|
|
1115
|
+
? workflowResult(request.command, "completed", resultMessage.message)
|
|
1116
|
+
: workflowResult(request.command, "completed", resultMessage.message, undefined, "status", [...messages, resultMessage]);
|
|
1007
1117
|
}
|
|
1008
1118
|
case "scoped-models": {
|
|
1009
1119
|
if (!selection)
|
|
@@ -1030,27 +1140,71 @@ export class PiEngineAdapter {
|
|
|
1030
1140
|
return workflowConfirmation(request.command, `Replace current session with ${path}?`);
|
|
1031
1141
|
}
|
|
1032
1142
|
if (!request.confirmed)
|
|
1033
|
-
return workflowResult(request.command, "cancelled", "Import cancelled");
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1143
|
+
return workflowResult(request.command, "cancelled", "Import cancelled", undefined, "status");
|
|
1144
|
+
try {
|
|
1145
|
+
const result = await requireCapability(runtime.importFromJsonl, "importFromJsonl").call(runtime, path, request.cwdOverride);
|
|
1146
|
+
if (isRecord(result) && result.cancelled === true)
|
|
1147
|
+
return workflowResult(request.command, "cancelled", "Import cancelled", undefined, "status");
|
|
1148
|
+
return workflowResult(request.command, "completed", `Session imported from: ${path}`);
|
|
1149
|
+
}
|
|
1150
|
+
catch (error) {
|
|
1151
|
+
const issue = isRecord(error) && isRecord(error.issue) ? error.issue : undefined;
|
|
1152
|
+
const fallbackCwd = issue === undefined ? undefined : stringProperty(issue, "fallbackCwd");
|
|
1153
|
+
if (fallbackCwd !== undefined && request.cwdOverride === undefined) {
|
|
1154
|
+
const sessionCwd = stringProperty(issue, "sessionCwd") ?? "the session working directory";
|
|
1155
|
+
return workflowConfirmation(request.command, `cwd from session file does not exist\n${sessionCwd}\n\ncontinue in current cwd\n${fallbackCwd}`, fallbackCwd);
|
|
1156
|
+
}
|
|
1157
|
+
throw error;
|
|
1158
|
+
}
|
|
1038
1159
|
}
|
|
1039
1160
|
case "share": {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1161
|
+
const signal = request.signal;
|
|
1162
|
+
if (signal?.aborted)
|
|
1163
|
+
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1164
|
+
let auth;
|
|
1165
|
+
try {
|
|
1166
|
+
auth = await this.#workflowHost.runCommand("gh", ["auth", "status"], signal === undefined ? undefined : { signal });
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
if (signal?.aborted || isAbortError(error))
|
|
1170
|
+
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1171
|
+
const message = commandMissing(error)
|
|
1172
|
+
? "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/"
|
|
1173
|
+
: "GitHub CLI is not logged in. Run 'gh auth login' first.";
|
|
1174
|
+
return workflowResult(request.command, "failed", message);
|
|
1175
|
+
}
|
|
1176
|
+
if (auth.stderr && !auth.stdout) {
|
|
1177
|
+
return workflowResult(request.command, "failed", "GitHub CLI is not logged in. Run 'gh auth login' first.");
|
|
1178
|
+
}
|
|
1045
1179
|
const temporary = join(tmpdir(), `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}pi-session-${process.pid}.html`);
|
|
1046
1180
|
try {
|
|
1047
|
-
|
|
1048
|
-
|
|
1181
|
+
try {
|
|
1182
|
+
await requireCapability(session.exportToHtml, "exportToHtml").call(session, temporary);
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
return workflowResult(request.command, "failed", `Failed to export session: ${errorMessage(error, "Unknown error")}`);
|
|
1186
|
+
}
|
|
1187
|
+
if (signal?.aborted)
|
|
1188
|
+
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1189
|
+
let gist;
|
|
1190
|
+
try {
|
|
1191
|
+
gist = await this.#workflowHost.runCommand("gh", ["gist", "create", "--public=false", temporary], signal === undefined ? undefined : { signal });
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
if (signal?.aborted || isAbortError(error))
|
|
1195
|
+
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1196
|
+
return workflowResult(request.command, "failed", `Failed to create gist: ${errorMessage(error, "Unknown error")}`);
|
|
1197
|
+
}
|
|
1198
|
+
if (signal?.aborted)
|
|
1199
|
+
return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
|
|
1200
|
+
if (gist.stderr && !gist.stdout) {
|
|
1201
|
+
return workflowResult(request.command, "failed", `Failed to create gist: ${gist.stderr.trim() || "Unknown error"}`);
|
|
1202
|
+
}
|
|
1049
1203
|
const gistUrl = gist.stdout.trim();
|
|
1050
1204
|
const gistId = gistUrl.split("/").at(-1);
|
|
1051
1205
|
if (!gistId)
|
|
1052
|
-
|
|
1053
|
-
return workflowResult(request.command, "completed", `Share URL:
|
|
1206
|
+
return workflowResult(request.command, "failed", "Failed to parse gist ID from gh output");
|
|
1207
|
+
return workflowResult(request.command, "completed", `Share URL: ${shareViewerUrl(gistId)}`, gistUrl);
|
|
1054
1208
|
}
|
|
1055
1209
|
finally {
|
|
1056
1210
|
await rm(temporary, { force: true });
|
|
@@ -1069,7 +1223,7 @@ export class PiEngineAdapter {
|
|
|
1069
1223
|
const current = manager?.getSessionName();
|
|
1070
1224
|
return typeof current === "string"
|
|
1071
1225
|
? workflowResult(request.command, "completed", `Session name: ${current}`)
|
|
1072
|
-
: workflowResult(request.command, "failed", "Usage: /name <name>");
|
|
1226
|
+
: workflowResult(request.command, "failed", "Usage: /name <name>", undefined, "warning");
|
|
1073
1227
|
}
|
|
1074
1228
|
requireCapability(session.setSessionName, "setSessionName").call(session, argument);
|
|
1075
1229
|
const normalized = manager?.getSessionName();
|
|
@@ -1096,17 +1250,17 @@ export class PiEngineAdapter {
|
|
|
1096
1250
|
return workflowResult(request.command, "failed", "Fork requires the owned user-message controller");
|
|
1097
1251
|
const result = await requireCapability(runtime.fork, "fork").call(runtime, selection, { position: "before" });
|
|
1098
1252
|
if (isRecord(result) && result.cancelled === true)
|
|
1099
|
-
return workflowResult(request.command, "cancelled", "Fork cancelled");
|
|
1253
|
+
return workflowResult(request.command, "cancelled", "Fork cancelled", undefined, "silent");
|
|
1100
1254
|
return workflowResult(request.command, "completed", "Forked to new session");
|
|
1101
1255
|
}
|
|
1102
1256
|
case "clone": {
|
|
1103
1257
|
const manager = session.sessionManager;
|
|
1104
1258
|
const leaf = manager?.getLeafId?.();
|
|
1105
1259
|
if (typeof leaf !== "string")
|
|
1106
|
-
return workflowResult(request.command, "
|
|
1260
|
+
return workflowResult(request.command, "completed", "Nothing to clone yet", undefined, "status");
|
|
1107
1261
|
const result = await requireCapability(runtime.fork, "fork").call(runtime, leaf, { position: "at" });
|
|
1108
1262
|
if (isRecord(result) && result.cancelled === true)
|
|
1109
|
-
return workflowResult(request.command, "cancelled", "Clone cancelled");
|
|
1263
|
+
return workflowResult(request.command, "cancelled", "Clone cancelled", undefined, "silent");
|
|
1110
1264
|
return workflowResult(request.command, "completed", "Cloned to new session");
|
|
1111
1265
|
}
|
|
1112
1266
|
case "tree": {
|
|
@@ -1120,9 +1274,9 @@ export class PiEngineAdapter {
|
|
|
1120
1274
|
...(request.treeSummary.customInstructions === undefined ? {} : { customInstructions: request.treeSummary.customInstructions }),
|
|
1121
1275
|
});
|
|
1122
1276
|
if (isRecord(result) && result.aborted === true)
|
|
1123
|
-
return workflowResult(request.command, "cancelled", "Branch summarization cancelled");
|
|
1277
|
+
return workflowResult(request.command, "cancelled", "Branch summarization cancelled", undefined, "status");
|
|
1124
1278
|
if (isRecord(result) && result.cancelled === true)
|
|
1125
|
-
return workflowResult(request.command, "cancelled", "Navigation cancelled");
|
|
1279
|
+
return workflowResult(request.command, "cancelled", "Navigation cancelled", undefined, "status");
|
|
1126
1280
|
return workflowResult(request.command, "completed", "Navigated to selected point");
|
|
1127
1281
|
}
|
|
1128
1282
|
case "trust": {
|
|
@@ -1157,6 +1311,7 @@ export class PiEngineAdapter {
|
|
|
1157
1311
|
const authType = authTypeValue === "api_key" ? "api_key" : "oauth";
|
|
1158
1312
|
const provider = modelRuntime.getProvider?.(providerId);
|
|
1159
1313
|
const providerName = stringProperty(provider, "name") ?? providerId;
|
|
1314
|
+
const previousModel = session.model;
|
|
1160
1315
|
this.#workflowInteraction.startLogin?.({ providerId, providerName, authType });
|
|
1161
1316
|
try {
|
|
1162
1317
|
await requireCapability(modelRuntime.login, "login").call(modelRuntime, providerId, authType, {
|
|
@@ -1194,25 +1349,40 @@ export class PiEngineAdapter {
|
|
|
1194
1349
|
});
|
|
1195
1350
|
}
|
|
1196
1351
|
catch (error) {
|
|
1197
|
-
if (error instanceof Error && error.message === "Login cancelled")
|
|
1198
|
-
return workflowResult(request.command, "cancelled", "Login cancelled");
|
|
1199
|
-
|
|
1352
|
+
if (error instanceof Error && error.message === "Login cancelled") {
|
|
1353
|
+
return workflowResult(request.command, "cancelled", "Login cancelled", undefined, "silent");
|
|
1354
|
+
}
|
|
1355
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1356
|
+
const actionLabel = authType === "api_key" ? `Saved API key for ${providerName}` : `Logged in to ${providerName}`;
|
|
1357
|
+
const message = error instanceof CredentialSynchronizationError
|
|
1358
|
+
? `${actionLabel}, but local model state could not be synchronized: ${detail}`
|
|
1359
|
+
: authType === "api_key"
|
|
1360
|
+
? `Failed to save API key for ${providerName}: ${detail}`
|
|
1361
|
+
: `Failed to login to ${providerName}: ${detail}`;
|
|
1362
|
+
return workflowResult(request.command, "failed", message);
|
|
1200
1363
|
}
|
|
1201
1364
|
finally {
|
|
1202
1365
|
this.#workflowInteraction.finishLogin?.();
|
|
1203
1366
|
}
|
|
1204
|
-
this.#
|
|
1205
|
-
this.#emitView();
|
|
1206
|
-
return workflowResult(request.command, "completed", `Logged in to ${providerName}. Credentials saved to ${join(this.#agentDir, "auth.json")}`);
|
|
1367
|
+
return await this.#completeProviderAuthentication(providerId, providerName, authType, previousModel);
|
|
1207
1368
|
}
|
|
1208
1369
|
case "logout": {
|
|
1209
1370
|
if (!selection)
|
|
1210
1371
|
return workflowResult(request.command, "failed", "Logout requires the owned authentication controller");
|
|
1211
1372
|
const [credentialType = "oauth", providerId = selection] = selection.includes(":") ? selection.split(":", 2) : ["oauth", selection];
|
|
1212
1373
|
const modelRuntime = runtime.services.modelRuntime;
|
|
1213
|
-
await requireCapability(modelRuntime.logout, "logout").call(modelRuntime, providerId, { signal: AbortSignal.timeout(15_000) });
|
|
1214
1374
|
const provider = modelRuntime.getProvider?.(providerId);
|
|
1215
1375
|
const providerName = stringProperty(provider, "name") ?? providerId;
|
|
1376
|
+
try {
|
|
1377
|
+
await requireCapability(modelRuntime.logout, "logout").call(modelRuntime, providerId, { signal: AbortSignal.timeout(15_000) });
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1381
|
+
const message = error instanceof CredentialSynchronizationError
|
|
1382
|
+
? `Credentials removed for ${providerName}, but local model state could not be synchronized: ${detail}`
|
|
1383
|
+
: `Logout failed: ${detail}`;
|
|
1384
|
+
return workflowResult(request.command, "failed", message);
|
|
1385
|
+
}
|
|
1216
1386
|
this.#reconcileActiveModelAvailability();
|
|
1217
1387
|
this.#emitView();
|
|
1218
1388
|
return workflowResult(request.command, "completed", credentialType === "api_key"
|
|
@@ -1222,12 +1392,17 @@ export class PiEngineAdapter {
|
|
|
1222
1392
|
case "new": {
|
|
1223
1393
|
const result = await runtime.newSession();
|
|
1224
1394
|
if (isRecord(result) && result.cancelled === true)
|
|
1225
|
-
return workflowResult(request.command, "cancelled", "New session cancelled");
|
|
1226
|
-
return workflowResult(request.command, "completed", "✓ New session started");
|
|
1395
|
+
return workflowResult(request.command, "cancelled", "New session cancelled", undefined, "silent");
|
|
1396
|
+
return workflowResult(request.command, "completed", "✓ New session started", undefined, "accent");
|
|
1227
1397
|
}
|
|
1228
1398
|
case "compact": {
|
|
1229
|
-
|
|
1230
|
-
|
|
1399
|
+
try {
|
|
1400
|
+
await session.compact(argument || undefined);
|
|
1401
|
+
return workflowResult(request.command, "completed", "Compaction requested", undefined, "silent");
|
|
1402
|
+
}
|
|
1403
|
+
catch (error) {
|
|
1404
|
+
return workflowResult(request.command, "failed", error instanceof Error ? error.message : String(error), undefined, "silent");
|
|
1405
|
+
}
|
|
1231
1406
|
}
|
|
1232
1407
|
case "resume": {
|
|
1233
1408
|
if (!selection && !argument)
|
|
@@ -1236,7 +1411,7 @@ export class PiEngineAdapter {
|
|
|
1236
1411
|
try {
|
|
1237
1412
|
const result = await runtime.switchSession(sessionPath);
|
|
1238
1413
|
if (isRecord(result) && result.cancelled === true)
|
|
1239
|
-
return workflowResult(request.command, "cancelled", "Resume cancelled");
|
|
1414
|
+
return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "silent");
|
|
1240
1415
|
return workflowResult(request.command, "completed", "Resumed session");
|
|
1241
1416
|
}
|
|
1242
1417
|
catch (error) {
|
|
@@ -1249,18 +1424,18 @@ export class PiEngineAdapter {
|
|
|
1249
1424
|
return workflowConfirmation(request.command, `cwd from session file does not exist\n${sessionCwd}\n\ncontinue in current cwd\n${fallbackCwd}`);
|
|
1250
1425
|
}
|
|
1251
1426
|
if (!request.confirmed)
|
|
1252
|
-
return workflowResult(request.command, "cancelled", "Resume cancelled");
|
|
1427
|
+
return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "status");
|
|
1253
1428
|
const result = await runtime.switchSession(sessionPath, { cwdOverride: fallbackCwd });
|
|
1254
1429
|
if (isRecord(result) && result.cancelled === true)
|
|
1255
|
-
return workflowResult(request.command, "cancelled", "Resume cancelled");
|
|
1430
|
+
return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "silent");
|
|
1256
1431
|
return workflowResult(request.command, "completed", "Resumed session in current cwd");
|
|
1257
1432
|
}
|
|
1258
1433
|
}
|
|
1259
1434
|
case "reload": {
|
|
1260
1435
|
if (session.isStreaming)
|
|
1261
|
-
return workflowResult(request.command, "failed", "Wait for the current response to finish before reloading.");
|
|
1436
|
+
return workflowResult(request.command, "failed", "Wait for the current response to finish before reloading.", undefined, "warning");
|
|
1262
1437
|
if (session.isCompacting)
|
|
1263
|
-
return workflowResult(request.command, "failed", "Wait for compaction to finish before reloading.");
|
|
1438
|
+
return workflowResult(request.command, "failed", "Wait for compaction to finish before reloading.", undefined, "warning");
|
|
1264
1439
|
await requireCapability(session.reload, "reload").call(session);
|
|
1265
1440
|
await this.#bindExtensionUiToSession();
|
|
1266
1441
|
return workflowResult(request.command, "completed", "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
|
|
@@ -1281,6 +1456,91 @@ export class PiEngineAdapter {
|
|
|
1281
1456
|
return workflowResult(request.command, "completed", "Demented elves announcement");
|
|
1282
1457
|
}
|
|
1283
1458
|
}
|
|
1459
|
+
async #completeProviderAuthentication(providerId, providerName, authType, previousModel) {
|
|
1460
|
+
const session = this.#requireWorkflowSession();
|
|
1461
|
+
const modelRuntime = this.#runtime?.services.modelRuntime;
|
|
1462
|
+
if (!modelRuntime)
|
|
1463
|
+
throw new Error("engine runtime is unavailable");
|
|
1464
|
+
const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
|
|
1465
|
+
let selectedModelId;
|
|
1466
|
+
let selectionError;
|
|
1467
|
+
if (isUnknownModel(previousModel)) {
|
|
1468
|
+
const available = modelRuntime.getAvailableSnapshot?.() ?? [];
|
|
1469
|
+
const providerModels = available.filter(model => stringProperty(model, "provider") === providerId);
|
|
1470
|
+
const defaultModelId = PINNED_DEFAULT_MODEL_BY_PROVIDER[providerId];
|
|
1471
|
+
if (defaultModelId === undefined) {
|
|
1472
|
+
selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
|
|
1473
|
+
}
|
|
1474
|
+
else if (providerModels.length === 0) {
|
|
1475
|
+
selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
const selectedModel = providerModels.find(model => stringProperty(model, "id") === defaultModelId);
|
|
1479
|
+
if (selectedModel === undefined) {
|
|
1480
|
+
selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
try {
|
|
1484
|
+
await session.setModel(selectedModel);
|
|
1485
|
+
selectedModelId = stringProperty(selectedModel, "id") ?? defaultModelId;
|
|
1486
|
+
this.#activeModel = {
|
|
1487
|
+
providerId,
|
|
1488
|
+
modelId: selectedModelId,
|
|
1489
|
+
displayName: stringProperty(selectedModel, "name") ?? selectedModelId,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
selectionError = `${actionLabel}, but selecting its default model failed: ${error instanceof Error ? error.message : String(error)}. Use /model to select a model.`;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
this.#reconcileActiveModelAvailability();
|
|
1499
|
+
this.#emitView();
|
|
1500
|
+
const status = `${actionLabel}.${selectedModelId ? ` Selected ${selectedModelId}.` : ""} Credentials saved to ${join(this.#agentDir, "auth.json")}`;
|
|
1501
|
+
const messages = [
|
|
1502
|
+
{ kind: "status", message: status },
|
|
1503
|
+
...(selectionError === undefined ? [] : [{ kind: "error", message: selectionError }]),
|
|
1504
|
+
];
|
|
1505
|
+
this.#scheduleAuthenticatedProviderRefresh(providerId, actionLabel);
|
|
1506
|
+
return workflowResult("login", "completed", status, undefined, "status", messages);
|
|
1507
|
+
}
|
|
1508
|
+
#scheduleAuthenticatedProviderRefresh(providerId, actionLabel) {
|
|
1509
|
+
const runtime = this.#runtime;
|
|
1510
|
+
const refresh = runtime?.services.modelRuntime.refresh;
|
|
1511
|
+
if (!runtime || typeof refresh !== "function")
|
|
1512
|
+
return;
|
|
1513
|
+
const generation = this.#sessionGeneration;
|
|
1514
|
+
const publish = this.#workflowInteraction.publish;
|
|
1515
|
+
const controller = new AbortController();
|
|
1516
|
+
const timeout = setTimeout(() => controller.abort(), AUTH_REFRESH_TIMEOUT_MS);
|
|
1517
|
+
// Compatibility: post-authentication refresh starts on the next turn so its notices follow the saved-credential status.
|
|
1518
|
+
void new Promise(resolve => setTimeout(resolve, 0))
|
|
1519
|
+
.then(() => refresh.call(runtime.services.modelRuntime, { providers: [providerId], signal: controller.signal }))
|
|
1520
|
+
.then(result => {
|
|
1521
|
+
if (this.#disposed || this.#sessionGeneration !== generation)
|
|
1522
|
+
return;
|
|
1523
|
+
if (isRecord(result) && result.aborted === true) {
|
|
1524
|
+
publish?.({ kind: "warning", message: `${actionLabel}, but its model catalog refresh timed out; using cached models.` });
|
|
1525
|
+
}
|
|
1526
|
+
else if (isRecord(result) && result.errors instanceof Map && result.errors.size > 0) {
|
|
1527
|
+
publish?.({ kind: "warning", message: `${actionLabel}, but its model catalog could not be refreshed; using cached models.` });
|
|
1528
|
+
}
|
|
1529
|
+
this.#reconcileActiveModelAvailability();
|
|
1530
|
+
this.#emitView();
|
|
1531
|
+
})
|
|
1532
|
+
.catch(error => {
|
|
1533
|
+
if (this.#disposed || this.#sessionGeneration !== generation)
|
|
1534
|
+
return;
|
|
1535
|
+
publish?.({
|
|
1536
|
+
kind: "warning",
|
|
1537
|
+
message: controller.signal.aborted
|
|
1538
|
+
? `${actionLabel}, but its model catalog refresh timed out; using cached models.`
|
|
1539
|
+
: `${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1540
|
+
});
|
|
1541
|
+
})
|
|
1542
|
+
.finally(() => clearTimeout(timeout));
|
|
1543
|
+
}
|
|
1284
1544
|
#requireWorkflowSession() {
|
|
1285
1545
|
if (this.#disposed || !this.#runtime || !this.#session)
|
|
1286
1546
|
throw new Error("engine adapter is not running");
|
|
@@ -2204,6 +2464,8 @@ async function createDefaultPiRuntime(input) {
|
|
|
2204
2464
|
cwd: input.cwd,
|
|
2205
2465
|
agentDir: input.agentDir,
|
|
2206
2466
|
...(input.sessionPath === undefined ? {} : { sessionPath: input.sessionPath }),
|
|
2467
|
+
...(input.sessionSelection === undefined ? {} : { sessionSelection: input.sessionSelection }),
|
|
2468
|
+
...(input.sessionForkPrompt === undefined ? {} : { sessionForkPrompt: input.sessionForkPrompt }),
|
|
2207
2469
|
...(input.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: input.projectTrustPrompt }),
|
|
2208
2470
|
});
|
|
2209
2471
|
}
|
|
@@ -2328,8 +2590,8 @@ function pinnedCacheWaste(entries, modelRuntime) {
|
|
|
2328
2590
|
function defaultWorkflowHost() {
|
|
2329
2591
|
return {
|
|
2330
2592
|
copyText: copyToClipboard,
|
|
2331
|
-
async runCommand(command, arguments_) {
|
|
2332
|
-
const result = await execFileAsync(command, [...arguments_], { encoding: "utf8" });
|
|
2593
|
+
async runCommand(command, arguments_, options) {
|
|
2594
|
+
const result = await execFileAsync(command, [...arguments_], { encoding: "utf8", signal: options?.signal });
|
|
2333
2595
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
2334
2596
|
},
|
|
2335
2597
|
readChangelog: async () => "No changelog entries found.",
|
|
@@ -2365,14 +2627,22 @@ function workflowLoginNotification(event) {
|
|
|
2365
2627
|
}
|
|
2366
2628
|
return { type: event.type === "waiting" ? "waiting" : "progress", message };
|
|
2367
2629
|
}
|
|
2368
|
-
function workflowResult(command, outcome, message, detail) {
|
|
2369
|
-
return {
|
|
2630
|
+
function workflowResult(command, outcome, message, detail, messageKind, messages) {
|
|
2631
|
+
return {
|
|
2632
|
+
command,
|
|
2633
|
+
outcome,
|
|
2634
|
+
message,
|
|
2635
|
+
...(detail === undefined ? {} : { detail }),
|
|
2636
|
+
...(messageKind === undefined ? {} : { messageKind }),
|
|
2637
|
+
...(messages === undefined ? {} : { messages: Object.freeze([...messages]) }),
|
|
2638
|
+
};
|
|
2370
2639
|
}
|
|
2371
|
-
function workflowConfirmation(command, message) {
|
|
2640
|
+
function workflowConfirmation(command, message, detail) {
|
|
2372
2641
|
return {
|
|
2373
2642
|
command,
|
|
2374
2643
|
outcome: "requires-confirmation",
|
|
2375
2644
|
message,
|
|
2645
|
+
...(detail === undefined ? {} : { detail }),
|
|
2376
2646
|
selectorTitle: "Confirm",
|
|
2377
2647
|
options: [
|
|
2378
2648
|
{ id: "yes", label: "Yes" },
|
|
@@ -2380,6 +2650,19 @@ function workflowConfirmation(command, message) {
|
|
|
2380
2650
|
],
|
|
2381
2651
|
};
|
|
2382
2652
|
}
|
|
2653
|
+
function errorMessage(error, fallback) {
|
|
2654
|
+
return error instanceof Error ? error.message : fallback;
|
|
2655
|
+
}
|
|
2656
|
+
function commandMissing(error) {
|
|
2657
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
2658
|
+
}
|
|
2659
|
+
function isAbortError(error) {
|
|
2660
|
+
return isRecord(error) && (error.name === "AbortError" || error.code === "ABORT_ERR");
|
|
2661
|
+
}
|
|
2662
|
+
function shareViewerUrl(gistId) {
|
|
2663
|
+
const baseUrl = process.env.PI_SHARE_VIEWER_URL || "https://pi.dev/session/";
|
|
2664
|
+
return `${baseUrl}#${gistId}`;
|
|
2665
|
+
}
|
|
2383
2666
|
function workflowOptions(value, idKey, labelKey) {
|
|
2384
2667
|
if (!Array.isArray(value))
|
|
2385
2668
|
return [];
|
|
@@ -2598,6 +2881,24 @@ function stringProperty(value, key) {
|
|
|
2598
2881
|
const item = value[key];
|
|
2599
2882
|
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
2600
2883
|
}
|
|
2884
|
+
function isUnknownModel(value) {
|
|
2885
|
+
return isRecord(value)
|
|
2886
|
+
&& value.provider === "unknown"
|
|
2887
|
+
&& value.id === "unknown"
|
|
2888
|
+
&& value.api === "unknown";
|
|
2889
|
+
}
|
|
2890
|
+
function findExactWorkflowModel(reference, models) {
|
|
2891
|
+
const normalized = reference.trim().toLowerCase();
|
|
2892
|
+
const canonical = models.filter(model => {
|
|
2893
|
+
const provider = stringProperty(model, "provider");
|
|
2894
|
+
const id = stringProperty(model, "id");
|
|
2895
|
+
return provider !== undefined && id !== undefined && `${provider}/${id}`.toLowerCase() === normalized;
|
|
2896
|
+
});
|
|
2897
|
+
if (canonical.length === 1)
|
|
2898
|
+
return canonical[0];
|
|
2899
|
+
const bare = models.filter(model => stringProperty(model, "id")?.toLowerCase() === normalized);
|
|
2900
|
+
return bare.length === 1 ? bare[0] : undefined;
|
|
2901
|
+
}
|
|
2601
2902
|
/**
|
|
2602
2903
|
* Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
|
|
2603
2904
|
* their unprefixed command. A1 presents the product-neutral command once while
|