@averyyy/pi-coding-agent 0.80.3-piclient.8 → 0.80.6-piclient.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +5 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts +4 -2
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/core/agent-session.d.ts +21 -5
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +100 -30
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +13 -5
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/cache-stats.d.ts +49 -0
- package/dist/core/cache-stats.d.ts.map +1 -0
- package/dist/core/cache-stats.js +101 -0
- package/dist/core/cache-stats.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +1 -2
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +56 -81
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +6 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +6 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +43 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +33 -11
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +22 -3
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/keybindings.d.ts +8 -3
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +10 -9
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -6
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +39 -9
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +10 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +14 -17
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +4 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +75 -28
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/pi-client-runtime-update.d.ts +14 -0
- package/dist/core/pi-client-runtime-update.d.ts.map +1 -0
- package/dist/core/pi-client-runtime-update.js +27 -0
- package/dist/core/pi-client-runtime-update.js.map +1 -0
- package/dist/core/pi-server-client.d.ts.map +1 -1
- package/dist/core/pi-server-client.js +122 -5
- package/dist/core/pi-server-client.js.map +1 -1
- package/dist/core/provider-attribution.d.ts.map +1 -1
- package/dist/core/provider-attribution.js +0 -10
- package/dist/core/provider-attribution.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +6 -7
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +8 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +20 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +97 -68
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +9 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +10 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +3 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +2 -2
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +2 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts +34 -3
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +280 -25
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +1 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
- package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
- package/dist/modes/interactive/components/custom-entry.js +52 -0
- package/dist/modes/interactive/components/custom-entry.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +4 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +9 -2
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +17 -6
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +12 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +5 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +49 -12
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +16 -5
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +368 -97
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -0
- package/dist/modes/interactive/theme/light.json +1 -0
- package/dist/modes/interactive/theme/theme-schema.json +6 -2
- package/dist/modes/interactive/theme/theme.d.ts +3 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +10 -3
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +1 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +6 -6
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +3 -3
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +4 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts +2 -2
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +71 -17
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -1
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +2 -0
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +13 -0
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/custom-provider.md +6 -3
- package/docs/extensions.md +213 -18
- package/docs/keybindings.md +1 -0
- package/docs/models.md +51 -8
- package/docs/packages.md +2 -2
- package/docs/providers.md +3 -0
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +15 -5
- package/docs/sdk.md +51 -1
- package/docs/session-format.md +18 -8
- package/docs/settings.md +3 -2
- package/docs/themes.md +5 -3
- package/docs/tui.md +1 -1
- package/docs/usage.md +3 -2
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/entry-renderer.ts +41 -0
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/preset.ts +2 -2
- package/examples/extensions/question.ts +1 -0
- package/examples/extensions/reload-runtime.ts +2 -2
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/12-full-control.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +8 -4
|
@@ -13,20 +13,22 @@ import { spawn, spawnSync } from "child_process";
|
|
|
13
13
|
import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
|
|
14
14
|
import { parseSkillBlock } from "../../core/agent-session.js";
|
|
15
15
|
import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
|
|
16
|
+
import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
|
|
16
17
|
import { FooterDataProvider } from "../../core/footer-data-provider.js";
|
|
17
18
|
import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
|
|
18
19
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
19
20
|
import { createCompactionSummaryMessage } from "../../core/messages.js";
|
|
20
21
|
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
|
|
21
22
|
import { DefaultPackageManager } from "../../core/package-manager.js";
|
|
23
|
+
import { getPiClientUpdateMarkerPath, PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE, readPiClientUpdateMarker, writePiClientRuntimeReloadState, } from "../../core/pi-client-runtime-update.js";
|
|
22
24
|
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
|
|
23
25
|
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
|
|
24
|
-
import { SessionManager } from "../../core/session-manager.js";
|
|
26
|
+
import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
|
|
25
27
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
26
28
|
import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
|
|
27
29
|
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
|
|
28
30
|
import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
|
|
29
|
-
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
31
|
+
import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
|
|
30
32
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
31
33
|
import { parseGitUrl } from "../../utils/git.js";
|
|
32
34
|
import { getCwdRelativePath } from "../../utils/paths.js";
|
|
@@ -41,6 +43,7 @@ import { BorderedLoader } from "./components/bordered-loader.js";
|
|
|
41
43
|
import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
|
|
42
44
|
import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
|
|
43
45
|
import { CustomEditor } from "./components/custom-editor.js";
|
|
46
|
+
import { CustomEntryComponent } from "./components/custom-entry.js";
|
|
44
47
|
import { CustomMessageComponent } from "./components/custom-message.js";
|
|
45
48
|
import { DaxnutsComponent } from "./components/daxnuts.js";
|
|
46
49
|
import { DynamicBorder } from "./components/dynamic-border.js";
|
|
@@ -48,11 +51,11 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
|
|
|
48
51
|
import { ExtensionEditorComponent } from "./components/extension-editor.js";
|
|
49
52
|
import { ExtensionInputComponent } from "./components/extension-input.js";
|
|
50
53
|
import { ExtensionSelectorComponent } from "./components/extension-selector.js";
|
|
51
|
-
import { FooterComponent } from "./components/footer.js";
|
|
54
|
+
import { FooterComponent, formatTokens } from "./components/footer.js";
|
|
52
55
|
import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
|
|
53
56
|
import { LoginDialogComponent } from "./components/login-dialog.js";
|
|
54
57
|
import { ModelSelectorComponent } from "./components/model-selector.js";
|
|
55
|
-
import { OAuthSelectorComponent } from "./components/oauth-selector.js";
|
|
58
|
+
import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
|
|
56
59
|
import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
|
|
57
60
|
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
58
61
|
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
|
@@ -81,6 +84,9 @@ class ExpandableText extends Text {
|
|
|
81
84
|
this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
|
|
82
85
|
}
|
|
83
86
|
}
|
|
87
|
+
function isCustomSessionEntry(item) {
|
|
88
|
+
return "type" in item && item.type === "custom";
|
|
89
|
+
}
|
|
84
90
|
const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
|
|
85
91
|
function isDeadTerminalError(error) {
|
|
86
92
|
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
@@ -120,7 +126,6 @@ export function formatResumeCommand(sessionManager) {
|
|
|
120
126
|
function hasDefaultModelProvider(providerId) {
|
|
121
127
|
return providerId in defaultModelPerProvider;
|
|
122
128
|
}
|
|
123
|
-
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
|
124
129
|
const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
|
|
125
130
|
export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
|
|
126
131
|
if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
|
|
@@ -131,6 +136,42 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
|
|
|
131
136
|
}
|
|
132
137
|
return !oauthProviderIds.has(providerId);
|
|
133
138
|
}
|
|
139
|
+
const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
|
|
140
|
+
function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
|
|
141
|
+
const filtered = fuzzyFilter(items, prefix, getSearchText);
|
|
142
|
+
if (filtered.length === 0)
|
|
143
|
+
return null;
|
|
144
|
+
return filtered.map(toAutocompleteItem);
|
|
145
|
+
}
|
|
146
|
+
function getLoginProviderCompletionOptions(providerOptions) {
|
|
147
|
+
const byId = new Map();
|
|
148
|
+
for (const provider of providerOptions) {
|
|
149
|
+
const existing = byId.get(provider.id);
|
|
150
|
+
if (existing) {
|
|
151
|
+
if (!existing.authTypes.includes(provider.authType)) {
|
|
152
|
+
existing.authTypes.push(provider.authType);
|
|
153
|
+
existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
byId.set(provider.id, {
|
|
158
|
+
id: provider.id,
|
|
159
|
+
name: provider.name,
|
|
160
|
+
authTypes: [provider.authType],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
164
|
+
}
|
|
165
|
+
function getLoginProviderSearchText(provider) {
|
|
166
|
+
const authTypes = provider.authTypes
|
|
167
|
+
.map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
|
|
168
|
+
.join(" ");
|
|
169
|
+
return `${provider.id} ${provider.name} ${authTypes}`;
|
|
170
|
+
}
|
|
171
|
+
function formatLoginProviderCompletionDescription(provider) {
|
|
172
|
+
const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
|
|
173
|
+
return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
|
|
174
|
+
}
|
|
134
175
|
export class InteractiveMode {
|
|
135
176
|
runtimeHost;
|
|
136
177
|
ui;
|
|
@@ -317,6 +358,7 @@ export class InteractiveMode {
|
|
|
317
358
|
const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
|
|
318
359
|
name: command.name,
|
|
319
360
|
description: command.description,
|
|
361
|
+
...(command.argumentHint && { argumentHint: command.argumentHint }),
|
|
320
362
|
}));
|
|
321
363
|
const modelCommand = slashCommands.find((command) => command.name === "model");
|
|
322
364
|
if (modelCommand) {
|
|
@@ -334,17 +376,24 @@ export class InteractiveMode {
|
|
|
334
376
|
name: m.name,
|
|
335
377
|
label: `${m.provider}/${m.id}`,
|
|
336
378
|
}));
|
|
337
|
-
|
|
338
|
-
const filtered = fuzzyFilter(items, prefix, getModelSearchText);
|
|
339
|
-
if (filtered.length === 0)
|
|
340
|
-
return null;
|
|
341
|
-
return filtered.map((item) => ({
|
|
379
|
+
return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
|
|
342
380
|
value: item.label,
|
|
343
381
|
label: item.id,
|
|
344
382
|
description: item.provider,
|
|
345
383
|
}));
|
|
346
384
|
};
|
|
347
385
|
}
|
|
386
|
+
const loginCommand = slashCommands.find((command) => command.name === "login");
|
|
387
|
+
if (loginCommand) {
|
|
388
|
+
loginCommand.getArgumentCompletions = (prefix) => {
|
|
389
|
+
const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
|
|
390
|
+
return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
|
|
391
|
+
value: provider.id,
|
|
392
|
+
label: provider.id,
|
|
393
|
+
description: formatLoginProviderCompletionDescription(provider),
|
|
394
|
+
}));
|
|
395
|
+
};
|
|
396
|
+
}
|
|
348
397
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
349
398
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
350
399
|
name: cmd.name,
|
|
@@ -483,7 +532,7 @@ export class InteractiveMode {
|
|
|
483
532
|
rawKeyHint("!!", "to run bash (no context)"),
|
|
484
533
|
hint("app.message.followUp", "to queue follow-up"),
|
|
485
534
|
hint("app.message.dequeue", "to edit all queued messages"),
|
|
486
|
-
hint("app.clipboard.pasteImage", "to paste image"),
|
|
535
|
+
hint("app.clipboard.pasteImage", "to paste image (with text fallback)"),
|
|
487
536
|
rawKeyHint("drop files", "to attach"),
|
|
488
537
|
].join("\n");
|
|
489
538
|
const compactInstructions = [
|
|
@@ -1155,7 +1204,7 @@ export class InteractiveMode {
|
|
|
1155
1204
|
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
1156
1205
|
},
|
|
1157
1206
|
commandContextActions: {
|
|
1158
|
-
waitForIdle: () => this.session.
|
|
1207
|
+
waitForIdle: () => this.session.waitForIdle(),
|
|
1159
1208
|
newSession: async (options) => {
|
|
1160
1209
|
this.clearStatusIndicator();
|
|
1161
1210
|
try {
|
|
@@ -1206,7 +1255,7 @@ export class InteractiveMode {
|
|
|
1206
1255
|
},
|
|
1207
1256
|
shutdownHandler: () => {
|
|
1208
1257
|
this.shutdownRequested = true;
|
|
1209
|
-
if (
|
|
1258
|
+
if (this.session.isIdle) {
|
|
1210
1259
|
void this.shutdown();
|
|
1211
1260
|
}
|
|
1212
1261
|
},
|
|
@@ -1299,7 +1348,7 @@ export class InteractiveMode {
|
|
|
1299
1348
|
sessionManager: this.sessionManager,
|
|
1300
1349
|
modelRegistry: this.session.modelRegistry,
|
|
1301
1350
|
model: this.session.model,
|
|
1302
|
-
isIdle: () =>
|
|
1351
|
+
isIdle: () => this.session.isIdle,
|
|
1303
1352
|
isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
|
|
1304
1353
|
signal: this.session.agent.signal,
|
|
1305
1354
|
abort: () => {
|
|
@@ -1985,6 +2034,7 @@ export class InteractiveMode {
|
|
|
1985
2034
|
this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
|
|
1986
2035
|
this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
|
|
1987
2036
|
this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
|
|
2037
|
+
this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
|
|
1988
2038
|
this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
|
|
1989
2039
|
this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
|
|
1990
2040
|
this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
|
|
@@ -1998,26 +2048,30 @@ export class InteractiveMode {
|
|
|
1998
2048
|
this.updateEditorBorderColor();
|
|
1999
2049
|
}
|
|
2000
2050
|
};
|
|
2001
|
-
// Handle clipboard
|
|
2051
|
+
// Handle clipboard paste (triggered on Ctrl+V). Images are attached by path;
|
|
2052
|
+
// otherwise, paste plain text from the system clipboard.
|
|
2002
2053
|
this.defaultEditor.onPasteImage = () => {
|
|
2003
|
-
this.
|
|
2054
|
+
void this.handleClipboardPaste();
|
|
2004
2055
|
};
|
|
2005
2056
|
}
|
|
2006
|
-
async
|
|
2057
|
+
async handleClipboardPaste() {
|
|
2007
2058
|
try {
|
|
2008
2059
|
const image = await readClipboardImage();
|
|
2009
|
-
if (
|
|
2060
|
+
if (image) {
|
|
2061
|
+
const tmpDir = os.tmpdir();
|
|
2062
|
+
const ext = extensionForImageMimeType(image.mimeType) ?? "png";
|
|
2063
|
+
const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
|
|
2064
|
+
const filePath = path.join(tmpDir, fileName);
|
|
2065
|
+
fs.writeFileSync(filePath, Buffer.from(image.bytes));
|
|
2066
|
+
this.editor.insertTextAtCursor?.(filePath);
|
|
2067
|
+
this.ui.requestRender();
|
|
2010
2068
|
return;
|
|
2011
2069
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
fs.writeFileSync(filePath, Buffer.from(image.bytes));
|
|
2018
|
-
// Insert file path directly
|
|
2019
|
-
this.editor.insertTextAtCursor?.(filePath);
|
|
2020
|
-
this.ui.requestRender();
|
|
2070
|
+
const text = await readClipboardText();
|
|
2071
|
+
if (text) {
|
|
2072
|
+
this.editor.insertTextAtCursor?.(text);
|
|
2073
|
+
this.ui.requestRender();
|
|
2074
|
+
}
|
|
2021
2075
|
}
|
|
2022
2076
|
catch {
|
|
2023
2077
|
// Silently ignore clipboard errors (may not have permission, etc.)
|
|
@@ -2105,9 +2159,10 @@ export class InteractiveMode {
|
|
|
2105
2159
|
this.editor.setText("");
|
|
2106
2160
|
return;
|
|
2107
2161
|
}
|
|
2108
|
-
if (text === "/login") {
|
|
2109
|
-
|
|
2162
|
+
if (text === "/login" || text.startsWith("/login ")) {
|
|
2163
|
+
const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
|
|
2110
2164
|
this.editor.setText("");
|
|
2165
|
+
await this.handleLoginCommand(providerRef);
|
|
2111
2166
|
return;
|
|
2112
2167
|
}
|
|
2113
2168
|
if (text === "/logout") {
|
|
@@ -2131,6 +2186,12 @@ export class InteractiveMode {
|
|
|
2131
2186
|
await this.handleReloadCommand();
|
|
2132
2187
|
return;
|
|
2133
2188
|
}
|
|
2189
|
+
const pendingPiClientUpdate = readPiClientUpdateMarker();
|
|
2190
|
+
if (pendingPiClientUpdate) {
|
|
2191
|
+
this.showWarning(`pi-client ${pendingPiClientUpdate.version} is installed. Run /reload to continue this session with the new version.`);
|
|
2192
|
+
this.editor.setText(text);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2134
2195
|
if (text === "/debug") {
|
|
2135
2196
|
this.handleDebugCommand();
|
|
2136
2197
|
this.editor.setText("");
|
|
@@ -2241,6 +2302,12 @@ export class InteractiveMode {
|
|
|
2241
2302
|
this.updatePendingMessagesDisplay();
|
|
2242
2303
|
this.ui.requestRender();
|
|
2243
2304
|
break;
|
|
2305
|
+
case "entry_appended":
|
|
2306
|
+
if (event.entry.type === "custom") {
|
|
2307
|
+
this.addCustomEntryToChat(event.entry);
|
|
2308
|
+
this.ui.requestRender();
|
|
2309
|
+
}
|
|
2310
|
+
break;
|
|
2244
2311
|
case "session_info_changed":
|
|
2245
2312
|
this.updateTerminalTitle();
|
|
2246
2313
|
this.footer.invalidate();
|
|
@@ -2326,6 +2393,7 @@ export class InteractiveMode {
|
|
|
2326
2393
|
for (const [, component] of this.pendingTools.entries()) {
|
|
2327
2394
|
component.setArgsComplete();
|
|
2328
2395
|
}
|
|
2396
|
+
this.maybeShowCacheMissNotice(this.streamingMessage);
|
|
2329
2397
|
}
|
|
2330
2398
|
this.streamingComponent = undefined;
|
|
2331
2399
|
this.streamingMessage = undefined;
|
|
@@ -2376,9 +2444,11 @@ export class InteractiveMode {
|
|
|
2376
2444
|
this.streamingMessage = undefined;
|
|
2377
2445
|
}
|
|
2378
2446
|
this.pendingTools.clear();
|
|
2379
|
-
await this.checkShutdownRequested();
|
|
2380
2447
|
this.ui.requestRender();
|
|
2381
2448
|
break;
|
|
2449
|
+
case "agent_settled":
|
|
2450
|
+
await this.checkShutdownRequested();
|
|
2451
|
+
break;
|
|
2382
2452
|
case "compaction_start": {
|
|
2383
2453
|
if (this.settingsManager.getShowTerminalProgress()) {
|
|
2384
2454
|
this.ui.terminal.setProgress(true);
|
|
@@ -2486,6 +2556,25 @@ export class InteractiveMode {
|
|
|
2486
2556
|
this.lastStatusText = text;
|
|
2487
2557
|
this.ui.requestRender();
|
|
2488
2558
|
}
|
|
2559
|
+
addCustomEntryToChat(entry) {
|
|
2560
|
+
const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
|
|
2561
|
+
if (!renderer) {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
const component = new CustomEntryComponent(entry, renderer);
|
|
2565
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
2566
|
+
if (!component.hasContent()) {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (this.streamingComponent) {
|
|
2570
|
+
const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
|
|
2571
|
+
if (streamingIndex >= 0) {
|
|
2572
|
+
this.chatContainer.children.splice(streamingIndex, 0, component);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
this.chatContainer.addChild(component);
|
|
2577
|
+
}
|
|
2489
2578
|
addMessageToChat(message, options) {
|
|
2490
2579
|
switch (message.role) {
|
|
2491
2580
|
case "bashExecution": {
|
|
@@ -2563,20 +2652,24 @@ export class InteractiveMode {
|
|
|
2563
2652
|
}
|
|
2564
2653
|
}
|
|
2565
2654
|
}
|
|
2566
|
-
|
|
2567
|
-
* Render session context to chat. Used for initial load and rebuild after compaction.
|
|
2568
|
-
* @param sessionContext Session context to render
|
|
2569
|
-
* @param options.updateFooter Update footer state
|
|
2570
|
-
* @param options.populateHistory Add user messages to editor history
|
|
2571
|
-
*/
|
|
2572
|
-
renderSessionContext(sessionContext, options = {}) {
|
|
2655
|
+
renderSessionItems(items, options = {}) {
|
|
2573
2656
|
this.pendingTools.clear();
|
|
2574
2657
|
const renderedPendingTools = new Map();
|
|
2658
|
+
// Cache-miss notices are not persisted; re-derive them from the full entry
|
|
2659
|
+
// list and re-inject them after the assistant messages that paid for them.
|
|
2660
|
+
const cacheMisses = this.settingsManager.getShowCacheMissNotices()
|
|
2661
|
+
? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
|
|
2662
|
+
: new Map();
|
|
2575
2663
|
if (options.updateFooter) {
|
|
2576
2664
|
this.footer.invalidate();
|
|
2577
2665
|
this.updateEditorBorderColor();
|
|
2578
2666
|
}
|
|
2579
|
-
for (const
|
|
2667
|
+
for (const item of items) {
|
|
2668
|
+
if (isCustomSessionEntry(item)) {
|
|
2669
|
+
this.addCustomEntryToChat(item);
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
const message = item;
|
|
2580
2673
|
// Assistant messages need special handling for tool calls
|
|
2581
2674
|
if (message.role === "assistant") {
|
|
2582
2675
|
this.addMessageToChat(message);
|
|
@@ -2608,6 +2701,11 @@ export class InteractiveMode {
|
|
|
2608
2701
|
}
|
|
2609
2702
|
}
|
|
2610
2703
|
}
|
|
2704
|
+
if (message.stopReason !== "aborted" && message.stopReason !== "error") {
|
|
2705
|
+
const miss = cacheMisses.get(message);
|
|
2706
|
+
if (miss)
|
|
2707
|
+
this.addCacheMissNotice(miss);
|
|
2708
|
+
}
|
|
2611
2709
|
}
|
|
2612
2710
|
else if (message.role === "toolResult") {
|
|
2613
2711
|
// Match tool results to pending tool components
|
|
@@ -2627,10 +2725,53 @@ export class InteractiveMode {
|
|
|
2627
2725
|
}
|
|
2628
2726
|
this.ui.requestRender();
|
|
2629
2727
|
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Render session entries to chat. Used for initial load and rebuild after compaction.
|
|
2730
|
+
* @param entries Compaction-aware session entries to render
|
|
2731
|
+
* @param options.updateFooter Update footer state
|
|
2732
|
+
* @param options.populateHistory Add user messages to editor history
|
|
2733
|
+
*/
|
|
2734
|
+
renderSessionEntries(entries, options = {}) {
|
|
2735
|
+
const items = entries.flatMap((entry) => {
|
|
2736
|
+
if (entry.type === "custom") {
|
|
2737
|
+
return [entry];
|
|
2738
|
+
}
|
|
2739
|
+
return sessionEntryToContextMessages(entry);
|
|
2740
|
+
});
|
|
2741
|
+
this.renderSessionItems(items, options);
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* Show a transcript notice when a completed assistant message paid for a
|
|
2745
|
+
* significant cache miss. Only states observable facts: the miss itself,
|
|
2746
|
+
* a model switch, or an idle gap past the cache TTL.
|
|
2747
|
+
*/
|
|
2748
|
+
maybeShowCacheMissNotice(message) {
|
|
2749
|
+
if (!this.settingsManager.getShowCacheMissNotices())
|
|
2750
|
+
return;
|
|
2751
|
+
// Entries don't contain `message` yet: message_end fires before persistence.
|
|
2752
|
+
const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
|
|
2753
|
+
if (miss)
|
|
2754
|
+
this.addCacheMissNotice(miss);
|
|
2755
|
+
}
|
|
2756
|
+
addCacheMissNotice(miss) {
|
|
2757
|
+
if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
|
|
2758
|
+
return;
|
|
2759
|
+
const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
|
|
2760
|
+
const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
|
|
2761
|
+
let label = "Cache miss";
|
|
2762
|
+
if (miss.modelChanged) {
|
|
2763
|
+
label = "Cache miss after model switch";
|
|
2764
|
+
}
|
|
2765
|
+
else if (miss.idleMs >= CACHE_TTL_MS) {
|
|
2766
|
+
label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
|
|
2767
|
+
}
|
|
2768
|
+
const text = theme.fg("warning", `${label}: ${reBilled}`);
|
|
2769
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
2770
|
+
this.chatContainer.addChild(new Text(text, 1, 0));
|
|
2771
|
+
}
|
|
2630
2772
|
renderInitialMessages() {
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
this.renderSessionContext(context, {
|
|
2773
|
+
const entries = this.sessionManager.buildContextEntries();
|
|
2774
|
+
this.renderSessionEntries(entries, {
|
|
2634
2775
|
updateFooter: true,
|
|
2635
2776
|
populateHistory: true,
|
|
2636
2777
|
});
|
|
@@ -2666,8 +2807,7 @@ export class InteractiveMode {
|
|
|
2666
2807
|
}
|
|
2667
2808
|
rebuildChatFromMessages() {
|
|
2668
2809
|
this.chatContainer.clear();
|
|
2669
|
-
|
|
2670
|
-
this.renderSessionContext(context);
|
|
2810
|
+
this.renderSessionEntries(this.sessionManager.buildContextEntries());
|
|
2671
2811
|
}
|
|
2672
2812
|
// =========================================================================
|
|
2673
2813
|
// Key handlers
|
|
@@ -3266,6 +3406,7 @@ export class InteractiveMode {
|
|
|
3266
3406
|
doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
|
|
3267
3407
|
treeFilterMode: this.settingsManager.getTreeFilterMode(),
|
|
3268
3408
|
showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
|
|
3409
|
+
showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
|
|
3269
3410
|
defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
|
|
3270
3411
|
editorPaddingX: this.settingsManager.getEditorPaddingX(),
|
|
3271
3412
|
outputPad: this.settingsManager.getOutputPad(),
|
|
@@ -3341,6 +3482,10 @@ export class InteractiveMode {
|
|
|
3341
3482
|
this.chatContainer.clear();
|
|
3342
3483
|
this.rebuildChatFromMessages();
|
|
3343
3484
|
},
|
|
3485
|
+
onShowCacheMissNoticesChange: (shown) => {
|
|
3486
|
+
this.settingsManager.setShowCacheMissNotices(shown);
|
|
3487
|
+
this.rebuildChatFromMessages();
|
|
3488
|
+
},
|
|
3344
3489
|
onCollapseChangelogChange: (collapsed) => {
|
|
3345
3490
|
this.settingsManager.setCollapseChangelog(collapsed);
|
|
3346
3491
|
},
|
|
@@ -3630,19 +3775,17 @@ export class InteractiveMode {
|
|
|
3630
3775
|
const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
|
|
3631
3776
|
this.showSelector((done) => {
|
|
3632
3777
|
const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
|
|
3778
|
+
done();
|
|
3633
3779
|
try {
|
|
3634
3780
|
const result = await this.runtimeHost.fork(entryId);
|
|
3635
3781
|
if (result.cancelled) {
|
|
3636
|
-
done();
|
|
3637
3782
|
this.ui.requestRender();
|
|
3638
3783
|
return;
|
|
3639
3784
|
}
|
|
3640
3785
|
this.editor.setText(result.selectedText ?? "");
|
|
3641
|
-
done();
|
|
3642
3786
|
this.showStatus("Forked to new session");
|
|
3643
3787
|
}
|
|
3644
3788
|
catch (error) {
|
|
3645
|
-
done();
|
|
3646
3789
|
this.showError(error instanceof Error ? error.message : String(error));
|
|
3647
3790
|
}
|
|
3648
3791
|
}, () => {
|
|
@@ -3769,6 +3912,19 @@ export class InteractiveMode {
|
|
|
3769
3912
|
this.sessionManager.appendLabelChange(entryId, label);
|
|
3770
3913
|
this.ui.requestRender();
|
|
3771
3914
|
}, initialSelectedId, initialFilterMode);
|
|
3915
|
+
selector.onCopy = async (text) => {
|
|
3916
|
+
if (!text) {
|
|
3917
|
+
this.showError("Selected entry has no text to copy");
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
await copyToClipboard(text);
|
|
3922
|
+
this.showStatus("Copied selected message to clipboard");
|
|
3923
|
+
}
|
|
3924
|
+
catch (error) {
|
|
3925
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
3926
|
+
}
|
|
3927
|
+
};
|
|
3772
3928
|
return { component: selector, focus: selector };
|
|
3773
3929
|
});
|
|
3774
3930
|
}
|
|
@@ -3871,13 +4027,79 @@ export class InteractiveMode {
|
|
|
3871
4027
|
}
|
|
3872
4028
|
return options.sort((a, b) => a.name.localeCompare(b.name));
|
|
3873
4029
|
}
|
|
3874
|
-
|
|
4030
|
+
findLoginProviderOptions(providerRef) {
|
|
4031
|
+
const normalizedProviderRef = providerRef.trim().toLowerCase();
|
|
4032
|
+
if (!normalizedProviderRef) {
|
|
4033
|
+
return [];
|
|
4034
|
+
}
|
|
4035
|
+
return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
|
|
4036
|
+
provider.name.toLowerCase() === normalizedProviderRef);
|
|
4037
|
+
}
|
|
4038
|
+
async handleLoginCommand(providerRef) {
|
|
4039
|
+
if (!providerRef) {
|
|
4040
|
+
this.showLoginAuthTypeSelector();
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
const providerOptions = this.findLoginProviderOptions(providerRef);
|
|
4044
|
+
if (providerOptions.length === 1) {
|
|
4045
|
+
await this.startProviderLogin(providerOptions[0]);
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
if (providerOptions.length > 1) {
|
|
4049
|
+
const providerIds = new Set(providerOptions.map((provider) => provider.id));
|
|
4050
|
+
if (providerIds.size === 1) {
|
|
4051
|
+
this.showLoginAuthTypeSelector(providerOptions);
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
this.showLoginProviderSelector(undefined, providerRef);
|
|
4056
|
+
}
|
|
4057
|
+
async startProviderLogin(providerOption) {
|
|
4058
|
+
if (providerOption.authType === "oauth") {
|
|
4059
|
+
await this.showLoginDialog(providerOption.id, providerOption.name);
|
|
4060
|
+
}
|
|
4061
|
+
else {
|
|
4062
|
+
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
showLoginAuthTypeSelector(providerOptions) {
|
|
3875
4066
|
const subscriptionLabel = "Use a subscription";
|
|
3876
4067
|
const apiKeyLabel = "Use an API key";
|
|
4068
|
+
const availableAuthTypes = providerOptions
|
|
4069
|
+
? new Set(providerOptions.map((provider) => provider.authType))
|
|
4070
|
+
: new Set(["oauth", "api_key"]);
|
|
4071
|
+
const options = [];
|
|
4072
|
+
if (availableAuthTypes.has("oauth")) {
|
|
4073
|
+
options.push(subscriptionLabel);
|
|
4074
|
+
}
|
|
4075
|
+
if (availableAuthTypes.has("api_key")) {
|
|
4076
|
+
options.push(apiKeyLabel);
|
|
4077
|
+
}
|
|
4078
|
+
if (options.length === 0) {
|
|
4079
|
+
this.showStatus("No login methods available.");
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
if (providerOptions && options.length === 1) {
|
|
4083
|
+
const providerOption = providerOptions[0];
|
|
4084
|
+
if (providerOption) {
|
|
4085
|
+
void this.startProviderLogin(providerOption);
|
|
4086
|
+
}
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
const title = providerOptions?.[0]
|
|
4090
|
+
? `Select authentication method for ${providerOptions[0].name}:`
|
|
4091
|
+
: "Select authentication method:";
|
|
3877
4092
|
this.showSelector((done) => {
|
|
3878
|
-
const selector = new ExtensionSelectorComponent(
|
|
4093
|
+
const selector = new ExtensionSelectorComponent(title, options, (option) => {
|
|
3879
4094
|
done();
|
|
3880
4095
|
const authType = option === subscriptionLabel ? "oauth" : "api_key";
|
|
4096
|
+
if (providerOptions) {
|
|
4097
|
+
const providerOption = providerOptions.find((provider) => provider.authType === authType);
|
|
4098
|
+
if (providerOption) {
|
|
4099
|
+
void this.startProviderLogin(providerOption);
|
|
4100
|
+
}
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
3881
4103
|
this.showLoginProviderSelector(authType);
|
|
3882
4104
|
}, () => {
|
|
3883
4105
|
done();
|
|
@@ -3886,32 +4108,34 @@ export class InteractiveMode {
|
|
|
3886
4108
|
return { component: selector, focus: selector };
|
|
3887
4109
|
});
|
|
3888
4110
|
}
|
|
3889
|
-
showLoginProviderSelector(authType) {
|
|
4111
|
+
showLoginProviderSelector(authType, initialSearchInput) {
|
|
3890
4112
|
const providerOptions = this.getLoginProviderOptions(authType);
|
|
3891
4113
|
if (providerOptions.length === 0) {
|
|
3892
|
-
|
|
4114
|
+
const message = authType === "oauth"
|
|
4115
|
+
? "No subscription providers available."
|
|
4116
|
+
: authType === "api_key"
|
|
4117
|
+
? "No API key providers available."
|
|
4118
|
+
: "No login providers available.";
|
|
4119
|
+
this.showStatus(message);
|
|
3893
4120
|
return;
|
|
3894
4121
|
}
|
|
3895
4122
|
this.showSelector((done) => {
|
|
3896
|
-
const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
|
|
4123
|
+
const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
|
|
3897
4124
|
done();
|
|
3898
|
-
const providerOption = providerOptions.find((provider) => provider.id === providerId);
|
|
4125
|
+
const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
|
|
3899
4126
|
if (!providerOption) {
|
|
3900
4127
|
return;
|
|
3901
4128
|
}
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
this.
|
|
4129
|
+
await this.startProviderLogin(providerOption);
|
|
4130
|
+
}, () => {
|
|
4131
|
+
done();
|
|
4132
|
+
if (authType) {
|
|
4133
|
+
this.showLoginAuthTypeSelector();
|
|
3907
4134
|
}
|
|
3908
4135
|
else {
|
|
3909
|
-
|
|
4136
|
+
this.ui.requestRender();
|
|
3910
4137
|
}
|
|
3911
|
-
}, () =>
|
|
3912
|
-
done();
|
|
3913
|
-
this.showLoginAuthTypeSelector();
|
|
3914
|
-
}, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
|
|
4138
|
+
}, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
|
|
3915
4139
|
return { component: selector, focus: selector };
|
|
3916
4140
|
});
|
|
3917
4141
|
}
|
|
@@ -4001,30 +4225,18 @@ export class InteractiveMode {
|
|
|
4001
4225
|
}
|
|
4002
4226
|
}
|
|
4003
4227
|
}
|
|
4004
|
-
showBedrockSetupDialog(providerId, providerName) {
|
|
4005
|
-
const restoreEditor = () => {
|
|
4006
|
-
this.editorContainer.clear();
|
|
4007
|
-
this.editorContainer.addChild(this.editor);
|
|
4008
|
-
this.ui.setFocus(this.editor);
|
|
4009
|
-
this.ui.requestRender();
|
|
4010
|
-
};
|
|
4011
|
-
const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
|
|
4012
|
-
dialog.showInfo([
|
|
4013
|
-
theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
|
|
4014
|
-
theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
|
|
4015
|
-
theme.fg("muted", "See:"),
|
|
4016
|
-
theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
|
|
4017
|
-
]);
|
|
4018
|
-
this.editorContainer.clear();
|
|
4019
|
-
this.editorContainer.addChild(dialog);
|
|
4020
|
-
this.ui.setFocus(dialog);
|
|
4021
|
-
this.ui.requestRender();
|
|
4022
|
-
}
|
|
4023
4228
|
async showApiKeyLoginDialog(providerId, providerName) {
|
|
4024
4229
|
const previousModel = this.session.model;
|
|
4025
4230
|
const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
|
|
4026
4231
|
// Completion handled below
|
|
4027
4232
|
}, providerName);
|
|
4233
|
+
if (providerId === "amazon-bedrock") {
|
|
4234
|
+
dialog.showDetails([
|
|
4235
|
+
theme.fg("text", "You can also use an AWS profile, IAM keys, or role-based credentials."),
|
|
4236
|
+
theme.fg("muted", "See:"),
|
|
4237
|
+
theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
|
|
4238
|
+
]);
|
|
4239
|
+
}
|
|
4028
4240
|
this.editorContainer.clear();
|
|
4029
4241
|
this.editorContainer.addChild(dialog);
|
|
4030
4242
|
this.ui.setFocus(dialog);
|
|
@@ -4165,12 +4377,31 @@ export class InteractiveMode {
|
|
|
4165
4377
|
this.showWarning("Wait for compaction to finish before reloading.");
|
|
4166
4378
|
return;
|
|
4167
4379
|
}
|
|
4380
|
+
const pendingPiClientUpdate = readPiClientUpdateMarker();
|
|
4381
|
+
if (pendingPiClientUpdate) {
|
|
4382
|
+
const reloadStatePath = process.env.PI_CLIENT_RELOAD_STATE_PATH;
|
|
4383
|
+
if (!reloadStatePath) {
|
|
4384
|
+
this.showWarning(`pi-client ${pendingPiClientUpdate.version} is installed. Restart pi-client to load the new runtime.`);
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
writePiClientRuntimeReloadState(reloadStatePath, {
|
|
4388
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
4389
|
+
sessionDir: this.sessionManager.usesDefaultSessionDir() ? undefined : this.sessionManager.getSessionDir(),
|
|
4390
|
+
updateMarkerPath: getPiClientUpdateMarkerPath(),
|
|
4391
|
+
});
|
|
4392
|
+
this.isShuttingDown = true;
|
|
4393
|
+
this.themeController.disableAutoSync();
|
|
4394
|
+
await this.ui.terminal.drainInput(1000);
|
|
4395
|
+
this.stop();
|
|
4396
|
+
await this.runtimeHost.dispose();
|
|
4397
|
+
process.exit(PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE);
|
|
4398
|
+
}
|
|
4168
4399
|
this.resetExtensionUI();
|
|
4169
4400
|
const reloadBox = new Container();
|
|
4170
4401
|
const borderColor = (s) => theme.fg("border", s);
|
|
4171
4402
|
reloadBox.addChild(new DynamicBorder(borderColor));
|
|
4172
4403
|
reloadBox.addChild(new Spacer(1));
|
|
4173
|
-
reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
|
|
4404
|
+
reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
|
|
4174
4405
|
reloadBox.addChild(new Spacer(1));
|
|
4175
4406
|
reloadBox.addChild(new DynamicBorder(borderColor));
|
|
4176
4407
|
const previousEditor = this.editor;
|
|
@@ -4234,8 +4465,8 @@ export class InteractiveMode {
|
|
|
4234
4465
|
this.showError(`models.json error: ${modelsJsonError}`);
|
|
4235
4466
|
}
|
|
4236
4467
|
this.showStatus(savedImplicitProjectTrust
|
|
4237
|
-
? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
|
|
4238
|
-
: "Reloaded keybindings, extensions, skills, prompts, themes");
|
|
4468
|
+
? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
|
|
4469
|
+
: "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
|
|
4239
4470
|
dismissReloadBox(this.editor);
|
|
4240
4471
|
reloadBoxDismissed = true;
|
|
4241
4472
|
}
|
|
@@ -4456,6 +4687,26 @@ export class InteractiveMode {
|
|
|
4456
4687
|
handleSessionCommand() {
|
|
4457
4688
|
const stats = this.session.getSessionStats();
|
|
4458
4689
|
const sessionName = this.sessionManager.getSessionName();
|
|
4690
|
+
const entries = this.sessionManager.getEntries();
|
|
4691
|
+
const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
|
|
4692
|
+
// Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
|
|
4693
|
+
// resolves to a concrete responseModel), sorted by cost descending.
|
|
4694
|
+
const perModelMap = new Map();
|
|
4695
|
+
for (const entry of entries) {
|
|
4696
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
4697
|
+
continue;
|
|
4698
|
+
const message = entry.message;
|
|
4699
|
+
const usage = message.usage;
|
|
4700
|
+
const key = `${message.provider}/${message.responseModel ?? message.model}`;
|
|
4701
|
+
let bucket = perModelMap.get(key);
|
|
4702
|
+
if (!bucket) {
|
|
4703
|
+
bucket = { key, cost: 0, tokens: 0 };
|
|
4704
|
+
perModelMap.set(key, bucket);
|
|
4705
|
+
}
|
|
4706
|
+
bucket.cost += usage.cost.total;
|
|
4707
|
+
bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
4708
|
+
}
|
|
4709
|
+
const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
|
|
4459
4710
|
let info = `${theme.bold("Session Info")}\n\n`;
|
|
4460
4711
|
if (sessionName) {
|
|
4461
4712
|
info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
|
|
@@ -4463,24 +4714,42 @@ export class InteractiveMode {
|
|
|
4463
4714
|
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
4464
4715
|
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
4465
4716
|
info += `${theme.bold("Messages")}\n`;
|
|
4717
|
+
info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
|
|
4466
4718
|
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
4467
4719
|
info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
|
|
4468
|
-
info += `${theme.fg("dim", "
|
|
4469
|
-
info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
|
|
4470
|
-
info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
|
|
4720
|
+
info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
|
|
4471
4721
|
info += `${theme.bold("Tokens")}\n`;
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
|
|
4722
|
+
// "Input" is the full prompt volume. With cache activity, split it into
|
|
4723
|
+
// cached (served from cache) vs uncached (everything else) - the only
|
|
4724
|
+
// provider-independent split. Cache writes, where reported, are a detail
|
|
4725
|
+
// of the uncached portion.
|
|
4726
|
+
const { input, cacheRead, cacheWrite } = stats.tokens;
|
|
4727
|
+
const promptTokens = input + cacheRead + cacheWrite;
|
|
4728
|
+
info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
|
|
4729
|
+
if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
|
|
4730
|
+
const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
|
|
4731
|
+
info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
|
|
4732
|
+
const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
|
|
4733
|
+
info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
|
|
4479
4734
|
}
|
|
4735
|
+
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
|
|
4480
4736
|
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
4481
|
-
if (stats.cost > 0) {
|
|
4737
|
+
if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
|
|
4482
4738
|
info += `\n${theme.bold("Cost")}\n`;
|
|
4483
|
-
info += `${theme.fg("dim", "Total:")}
|
|
4739
|
+
info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
|
|
4740
|
+
if (perModel.length > 1) {
|
|
4741
|
+
for (const entry of perModel) {
|
|
4742
|
+
info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
if (cacheWaste.missedTokens > 0) {
|
|
4746
|
+
const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
|
|
4747
|
+
const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
|
|
4748
|
+
info +=
|
|
4749
|
+
cacheWaste.missedCost >= 0.0001
|
|
4750
|
+
? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
|
|
4751
|
+
: `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
|
|
4752
|
+
}
|
|
4484
4753
|
}
|
|
4485
4754
|
this.chatContainer.addChild(new Spacer(1));
|
|
4486
4755
|
this.chatContainer.addChild(new Text(info, 1, 0));
|
|
@@ -4552,6 +4821,7 @@ export class InteractiveMode {
|
|
|
4552
4821
|
const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
|
|
4553
4822
|
const externalEditor = this.getAppKeyDisplay("app.editor.external");
|
|
4554
4823
|
const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
|
|
4824
|
+
const copyMessage = this.getAppKeyDisplay("app.message.copy");
|
|
4555
4825
|
const followUp = this.getAppKeyDisplay("app.message.followUp");
|
|
4556
4826
|
const dequeue = this.getAppKeyDisplay("app.message.dequeue");
|
|
4557
4827
|
const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
|
|
@@ -4594,9 +4864,10 @@ export class InteractiveMode {
|
|
|
4594
4864
|
| \`${expandTools}\` | Toggle tool output expansion |
|
|
4595
4865
|
| \`${toggleThinking}\` | Toggle thinking block visibility |
|
|
4596
4866
|
| \`${externalEditor}\` | Edit message in external editor |
|
|
4867
|
+
| \`${copyMessage}\` | Copy last assistant message |
|
|
4597
4868
|
| \`${followUp}\` | Queue follow-up message |
|
|
4598
4869
|
| \`${dequeue}\` | Restore queued messages |
|
|
4599
|
-
| \`${pasteImage}\` | Paste image from clipboard |
|
|
4870
|
+
| \`${pasteImage}\` | Paste image or text from clipboard |
|
|
4600
4871
|
| \`/\` | Slash commands |
|
|
4601
4872
|
| \`!\` | Run bash command |
|
|
4602
4873
|
| \`!!\` | Run bash command (excluded from context) |
|