@oh-my-pi/pi-coding-agent 16.0.9 → 16.0.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 +23 -0
- package/dist/cli.js +2822 -2872
- package/dist/types/collab/host.d.ts +2 -2
- package/dist/types/collab/protocol.d.ts +4 -5
- package/dist/types/config/model-resolver.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +12 -2
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
- package/dist/types/tools/index.d.ts +9 -1
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/dist/types/utils/qrcode.d.ts +48 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -1
- package/src/collab/host.ts +4 -4
- package/src/collab/protocol.ts +48 -15
- package/src/config/config-file.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-registry.ts +16 -4
- package/src/config/model-resolver.ts +193 -35
- package/src/config/settings-schema.ts +14 -2
- package/src/config/settings.ts +3 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +2 -2
- package/src/modes/components/oauth-selector.ts +31 -2
- package/src/prompts/tools/inspect-image.md +1 -1
- package/src/sdk.ts +26 -7
- package/src/session/agent-session.ts +93 -14
- package/src/slash-commands/builtin-registry.ts +29 -11
- package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
- package/src/thinking.ts +25 -5
- package/src/tools/index.ts +10 -1
- package/src/tools/inspect-image.ts +72 -9
- package/src/utils/file-mentions.ts +5 -2
- package/src/utils/image-loading.ts +58 -0
- package/src/utils/qrcode.ts +535 -0
package/src/main.ts
CHANGED
|
@@ -72,7 +72,7 @@ import { shouldShowStartupSplash } from "./startup-splash";
|
|
|
72
72
|
import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
|
|
73
73
|
import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
|
|
74
74
|
import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
|
|
75
|
-
import { AUTO_THINKING } from "./thinking";
|
|
75
|
+
import { AUTO_THINKING, parseConfiguredThinkingLevel } from "./thinking";
|
|
76
76
|
import type { LspStartupServerInfo } from "./tools";
|
|
77
77
|
import {
|
|
78
78
|
getChangelogPath,
|
|
@@ -857,7 +857,7 @@ async function buildSessionOptions(
|
|
|
857
857
|
if (scopedModels.length > 0) {
|
|
858
858
|
// `auto` is a session-level concept only; per-scoped-model (Ctrl+P) thinking
|
|
859
859
|
// overrides stay concrete, so coerce the auto default to "unset" here.
|
|
860
|
-
const defaultThinkingLevelSetting = activeSettings.get("defaultThinkingLevel");
|
|
860
|
+
const defaultThinkingLevelSetting = parseConfiguredThinkingLevel(activeSettings.get("defaultThinkingLevel"));
|
|
861
861
|
const defaultThinkingLevel =
|
|
862
862
|
defaultThinkingLevelSetting === AUTO_THINKING ? undefined : defaultThinkingLevelSetting;
|
|
863
863
|
options.scopedModels = scopedModels.map(scopedModel => ({
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Spacer,
|
|
11
11
|
TruncatedText,
|
|
12
12
|
} from "@oh-my-pi/pi-tui";
|
|
13
|
+
import { settings } from "../../config/settings";
|
|
13
14
|
import { theme } from "../../modes/theme/theme";
|
|
14
15
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
15
16
|
import type { AuthStorage, CredentialOriginKind } from "../../session/auth-storage";
|
|
@@ -17,6 +18,20 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
17
18
|
|
|
18
19
|
const OAUTH_SELECTOR_MAX_VISIBLE = 10;
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Provider ids the user has disabled via settings. `/login` (login mode) hides
|
|
23
|
+
* these so a disabled provider's models stay out of reach end-to-end, mirroring
|
|
24
|
+
* the model picker's `disabledProviders` filtering. Reads the settings singleton
|
|
25
|
+
* defensively: it throws before `Settings.init()`, in which case nothing is disabled.
|
|
26
|
+
*/
|
|
27
|
+
function getDisabledProviderIds(): ReadonlySet<string> {
|
|
28
|
+
try {
|
|
29
|
+
return new Set(settings.get("disabledProviders"));
|
|
30
|
+
} catch {
|
|
31
|
+
return new Set();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
36
|
* Rendered lines before the provider rows: top border, spacer, title, spacer
|
|
22
37
|
* (must mirror the constructor's addChild order).
|
|
@@ -102,8 +117,22 @@ export class OAuthSelectorComponent extends Container {
|
|
|
102
117
|
|
|
103
118
|
#loadProviders(): void {
|
|
104
119
|
const providers = getOAuthProviders();
|
|
105
|
-
this.#
|
|
106
|
-
|
|
120
|
+
if (this.#mode === "logout") {
|
|
121
|
+
// Logout stays unfiltered by `disabledProviders`: a now-disabled
|
|
122
|
+
// provider may still hold stored credentials worth removing.
|
|
123
|
+
this.#allProviders = providers.filter(provider => this.#hasSelectableAuth(provider.id));
|
|
124
|
+
} else {
|
|
125
|
+
const disabled = getDisabledProviderIds();
|
|
126
|
+
// Hide a login entry when either its own id or the provider id it
|
|
127
|
+
// stores credentials under is disabled, so alias logins (e.g.
|
|
128
|
+
// `openai-codex-device` ⇒ `openai-codex`) disappear alongside the
|
|
129
|
+
// model provider they authenticate.
|
|
130
|
+
this.#allProviders = providers.filter(
|
|
131
|
+
provider =>
|
|
132
|
+
!disabled.has(provider.id) &&
|
|
133
|
+
!(provider.storeCredentialsAs && disabled.has(provider.storeCredentialsAs)),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
107
136
|
this.#filteredProviders = this.#allProviders;
|
|
108
137
|
}
|
|
109
138
|
|
|
@@ -2,7 +2,7 @@ Inspects an image file with a vision-capable model and returns compact text anal
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Use this for image understanding tasks (OCR, UI/screenshot debugging, scene/object questions)
|
|
5
|
-
- Provide `path`
|
|
5
|
+
- Provide `path` as a local image file path, `Image #N` attachment label, or `attachment://N` URI
|
|
6
6
|
- Write a specific `question`:
|
|
7
7
|
- what to inspect
|
|
8
8
|
- constraints (for example: "quote visible text verbatim", "only report confirmed findings")
|
package/src/sdk.ts
CHANGED
|
@@ -144,6 +144,7 @@ import { AgentOutputManager } from "./task/output-manager";
|
|
|
144
144
|
import {
|
|
145
145
|
AUTO_THINKING,
|
|
146
146
|
type ConfiguredThinkingLevel,
|
|
147
|
+
parseConfiguredThinkingLevel,
|
|
147
148
|
parseThinkingLevel,
|
|
148
149
|
resolveProvisionalAutoLevel,
|
|
149
150
|
resolveThinkingLevelForModel,
|
|
@@ -1266,12 +1267,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1266
1267
|
? getRestorableSessionModels(existingSession.models, sessionManager.getLastModelChangeRole())
|
|
1267
1268
|
: [];
|
|
1268
1269
|
let restoredSessionModelIndex = -1;
|
|
1270
|
+
let restoredSessionThinkingLevel: ThinkingLevel | undefined;
|
|
1269
1271
|
if (!hasExplicitModel && !model && sessionModelStrings.length > 0) {
|
|
1270
1272
|
logger.time("restoreSessionModel", () => {
|
|
1271
1273
|
let failedSessionModel: string | undefined;
|
|
1272
1274
|
for (let i = 0; i < sessionModelStrings.length; i++) {
|
|
1273
1275
|
const sessionModelStr = sessionModelStrings[i];
|
|
1274
|
-
const parsedModel = parseModelString(sessionModelStr
|
|
1276
|
+
const parsedModel = parseModelString(sessionModelStr, {
|
|
1277
|
+
allowMaxAlias: true,
|
|
1278
|
+
isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
|
|
1279
|
+
});
|
|
1275
1280
|
if (!parsedModel) {
|
|
1276
1281
|
failedSessionModel ??= sessionModelStr;
|
|
1277
1282
|
continue;
|
|
@@ -1281,6 +1286,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1281
1286
|
if (restoredModel && hasModelAuth(restoredModel)) {
|
|
1282
1287
|
model = restoredModel;
|
|
1283
1288
|
restoredSessionModelIndex = i;
|
|
1289
|
+
restoredSessionThinkingLevel = parsedModel.thinkingLevel;
|
|
1284
1290
|
break;
|
|
1285
1291
|
}
|
|
1286
1292
|
failedSessionModel ??= sessionModelStr;
|
|
@@ -1305,15 +1311,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1305
1311
|
const taskDepth = options.taskDepth ?? 0;
|
|
1306
1312
|
|
|
1307
1313
|
// Resolves the session/agent thinking level using the same precedence we
|
|
1308
|
-
// apply at startup: explicit option → persisted session entry →
|
|
1309
|
-
//
|
|
1310
|
-
// settings default. Run again after extension
|
|
1311
|
-
// model's own defaults aren't masked by an earlier
|
|
1314
|
+
// apply at startup: explicit option → persisted session entry → restored
|
|
1315
|
+
// model selector suffix → default role's explicit selector → selected
|
|
1316
|
+
// model's defaultLevel → global settings default. Run again after extension
|
|
1317
|
+
// role reclaim so the final model's own defaults aren't masked by an earlier
|
|
1318
|
+
// fallback model's.
|
|
1312
1319
|
const pickInitialThinkingLevel = (selectedModel: Model | undefined): ConfiguredThinkingLevel | undefined => {
|
|
1313
1320
|
let level = options.thinkingLevel;
|
|
1314
1321
|
if (level === undefined && hasExistingSession && hasThinkingEntry) {
|
|
1315
1322
|
level = parseThinkingLevel(existingSession.thinkingLevel);
|
|
1316
1323
|
}
|
|
1324
|
+
if (level === undefined && !hasThinkingEntry && restoredSessionThinkingLevel !== undefined) {
|
|
1325
|
+
level = restoredSessionThinkingLevel;
|
|
1326
|
+
}
|
|
1317
1327
|
if (level === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) {
|
|
1318
1328
|
level = defaultRoleSpec.thinkingLevel;
|
|
1319
1329
|
}
|
|
@@ -1321,7 +1331,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1321
1331
|
level = selectedModel.thinking.defaultLevel;
|
|
1322
1332
|
}
|
|
1323
1333
|
if (level === undefined) {
|
|
1324
|
-
level = settings.get("defaultThinkingLevel");
|
|
1334
|
+
level = parseConfiguredThinkingLevel(settings.get("defaultThinkingLevel"));
|
|
1325
1335
|
}
|
|
1326
1336
|
return level;
|
|
1327
1337
|
};
|
|
@@ -1533,6 +1543,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1533
1543
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
1534
1544
|
getActiveModelString,
|
|
1535
1545
|
getActiveModel: () => agent?.state.model ?? model,
|
|
1546
|
+
getImageAttachments: () => session?.getImageAttachments() ?? [],
|
|
1536
1547
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
1537
1548
|
getPlanReferencePath: () => session?.getPlanReferencePath() ?? "local://PLAN.md",
|
|
1538
1549
|
getGoalModeState: () => session?.getGoalModeState(),
|
|
@@ -1905,13 +1916,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1905
1916
|
if (!hasExplicitModel && sessionRetryLimit > 0) {
|
|
1906
1917
|
for (let i = 0; i < sessionRetryLimit; i++) {
|
|
1907
1918
|
const sessionModelStr = sessionModelStrings[i];
|
|
1908
|
-
const parsedModel = parseModelString(sessionModelStr
|
|
1919
|
+
const parsedModel = parseModelString(sessionModelStr, {
|
|
1920
|
+
allowMaxAlias: true,
|
|
1921
|
+
isLiteralModelId: (provider, id) => modelRegistry.find(provider, id) !== undefined,
|
|
1922
|
+
});
|
|
1909
1923
|
if (!parsedModel) continue;
|
|
1910
1924
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
1911
1925
|
if (restoredModel && hasModelAuth(restoredModel)) {
|
|
1912
1926
|
model = restoredModel;
|
|
1913
1927
|
modelFallbackMessage = undefined;
|
|
1914
1928
|
restoredSessionModelIndex = i;
|
|
1929
|
+
restoredSessionThinkingLevel = parsedModel.thinkingLevel;
|
|
1915
1930
|
// Recompute thinking-level from scratch against the reclaimed
|
|
1916
1931
|
// model: any value derived from the earlier fallback model's
|
|
1917
1932
|
// `thinking.defaultLevel` must not become sticky.
|
|
@@ -2585,6 +2600,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2585
2600
|
if (watchdogFiles && watchdogFiles.length > 0) {
|
|
2586
2601
|
advisorWatchdogPrompt = watchdogFiles.join("\n\n");
|
|
2587
2602
|
}
|
|
2603
|
+
// Owned only when this session created the manager; subagents receive a
|
|
2604
|
+
// parent's manager via `options.mcpManager` and MUST NOT disconnect it.
|
|
2605
|
+
const ownedMcpManager = options.mcpManager ? undefined : mcpManager;
|
|
2588
2606
|
session = new AgentSession({
|
|
2589
2607
|
advisorWatchdogPrompt,
|
|
2590
2608
|
agent,
|
|
@@ -2630,6 +2648,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2630
2648
|
return out;
|
|
2631
2649
|
}
|
|
2632
2650
|
: undefined,
|
|
2651
|
+
disconnectOwnedMcpManager: ownedMcpManager ? () => ownedMcpManager.disconnectAll() : undefined,
|
|
2633
2652
|
mcpDiscoveryEnabled,
|
|
2634
2653
|
initialSelectedMCPToolNames,
|
|
2635
2654
|
defaultSelectedMCPToolNames,
|
|
@@ -116,6 +116,7 @@ import {
|
|
|
116
116
|
prompt,
|
|
117
117
|
relativePathWithinRoot,
|
|
118
118
|
Snowflake,
|
|
119
|
+
withTimeout,
|
|
119
120
|
} from "@oh-my-pi/pi-utils";
|
|
120
121
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
121
122
|
import {
|
|
@@ -235,6 +236,7 @@ import {
|
|
|
235
236
|
AUTO_THINKING,
|
|
236
237
|
type ConfiguredThinkingLevel,
|
|
237
238
|
clampAutoThinkingEffort,
|
|
239
|
+
parseConfiguredThinkingLevel,
|
|
238
240
|
resolveProvisionalAutoLevel,
|
|
239
241
|
resolveThinkingLevelForModel,
|
|
240
242
|
shouldDisableReasoning,
|
|
@@ -511,6 +513,13 @@ export interface AgentSessionConfig {
|
|
|
511
513
|
advisorReadOnlyTools?: AgentTool[];
|
|
512
514
|
/** Preloaded watchdog prompt content for the advisor. */
|
|
513
515
|
advisorWatchdogPrompt?: string;
|
|
516
|
+
/**
|
|
517
|
+
* Disconnect this session's OWNED MCP manager on dispose. Provided only when
|
|
518
|
+
* the session created the manager (top-level sessions); subagents reuse a
|
|
519
|
+
* parent's manager via `options.mcpManager` and omit this so a child's
|
|
520
|
+
* teardown never tears down the shared servers.
|
|
521
|
+
*/
|
|
522
|
+
disconnectOwnedMcpManager?: () => Promise<void>;
|
|
514
523
|
}
|
|
515
524
|
|
|
516
525
|
/** Options for AgentSession.prompt() */
|
|
@@ -664,10 +673,16 @@ interface ActiveRetryFallbackState {
|
|
|
664
673
|
pinned: boolean;
|
|
665
674
|
}
|
|
666
675
|
|
|
667
|
-
function parseRetryFallbackSelector(
|
|
676
|
+
function parseRetryFallbackSelector(
|
|
677
|
+
selector: string,
|
|
678
|
+
modelLookup?: { find(provider: string, id: string): Model | undefined },
|
|
679
|
+
): RetryFallbackSelector | undefined {
|
|
668
680
|
const trimmed = selector.trim();
|
|
669
681
|
if (!trimmed) return undefined;
|
|
670
|
-
const parsed = parseModelString(trimmed
|
|
682
|
+
const parsed = parseModelString(trimmed, {
|
|
683
|
+
allowMaxAlias: true,
|
|
684
|
+
isLiteralModelId: (provider, id) => modelLookup?.find(provider, id) !== undefined,
|
|
685
|
+
});
|
|
671
686
|
if (!parsed) return undefined;
|
|
672
687
|
return {
|
|
673
688
|
raw: trimmed,
|
|
@@ -1195,6 +1210,7 @@ export class AgentSession {
|
|
|
1195
1210
|
| undefined;
|
|
1196
1211
|
#getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
|
|
1197
1212
|
#reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
|
|
1213
|
+
#disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
|
|
1198
1214
|
#requestedToolNames: ReadonlySet<string> | undefined;
|
|
1199
1215
|
#baseSystemPrompt: string[];
|
|
1200
1216
|
/**
|
|
@@ -1561,6 +1577,7 @@ export class AgentSession {
|
|
|
1561
1577
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
1562
1578
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1563
1579
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1580
|
+
this.#disconnectOwnedMcpManager = config.disconnectOwnedMcpManager;
|
|
1564
1581
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1565
1582
|
this.#promptModelKey = this.#currentPromptModelKey();
|
|
1566
1583
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
@@ -4044,6 +4061,30 @@ export class AgentSession {
|
|
|
4044
4061
|
this.#releasePowerAssertion();
|
|
4045
4062
|
await this.sessionManager.close();
|
|
4046
4063
|
this.#closeAllProviderSessions("dispose");
|
|
4064
|
+
// Disconnect the MCP manager this session OWNS so its stdio servers are
|
|
4065
|
+
// not orphaned at exit. Best-effort: a failure here must never throw out
|
|
4066
|
+
// of dispose. Only owning (top-level) sessions provide this callback;
|
|
4067
|
+
// subagents reuse a parent's manager and must not tear it down. Idempotent
|
|
4068
|
+
// with the deferred-discovery disconnect in `createAgentSession`.
|
|
4069
|
+
//
|
|
4070
|
+
// BOUNDED: an owned manager may hold an HTTP/SSE server whose session-
|
|
4071
|
+
// termination DELETE blocks up to the MCP request timeout (30s default,
|
|
4072
|
+
// unbounded when OMP_MCP_TIMEOUT_MS=0), so awaiting `disconnectAll()`
|
|
4073
|
+
// unbounded would stall /exit and print-mode shutdown on a broken remote
|
|
4074
|
+
// endpoint. Race it against a short deadline — stdio close (the subprocess
|
|
4075
|
+
// reap this targets) completes well within the bound; a slow transport
|
|
4076
|
+
// close is left to finish detached. Mirrors the bounded async-job teardown.
|
|
4077
|
+
if (this.#disconnectOwnedMcpManager) {
|
|
4078
|
+
try {
|
|
4079
|
+
await withTimeout(
|
|
4080
|
+
this.#disconnectOwnedMcpManager(),
|
|
4081
|
+
3_000,
|
|
4082
|
+
"Timed out disconnecting owned MCP manager during dispose",
|
|
4083
|
+
);
|
|
4084
|
+
} catch (error) {
|
|
4085
|
+
logger.warn("Failed to disconnect owned MCP manager during dispose", { error: String(error) });
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4047
4088
|
// Flush the retain queue BEFORE clearing the session's pointer so
|
|
4048
4089
|
// `HindsightRetainQueue.#doFlush` still sees `session.getHindsightSessionState() === state`.
|
|
4049
4090
|
// Reversed, the spliced batch survives just long enough to fail the
|
|
@@ -4938,6 +4979,24 @@ export class AgentSession {
|
|
|
4938
4979
|
return this.agent.state.messages;
|
|
4939
4980
|
}
|
|
4940
4981
|
|
|
4982
|
+
/** Latest image attachments addressable by tools as `Image #N` or `attachment://N`. */
|
|
4983
|
+
getImageAttachments(): { label: string; uri: string; image: ImageContent }[] {
|
|
4984
|
+
for (let i = this.agent.state.messages.length - 1; i >= 0; i--) {
|
|
4985
|
+
const message = this.agent.state.messages[i];
|
|
4986
|
+
if (!message || (message.role !== "user" && message.role !== "developer") || !Array.isArray(message.content)) {
|
|
4987
|
+
continue;
|
|
4988
|
+
}
|
|
4989
|
+
const images = message.content.filter((part): part is ImageContent => part.type === "image");
|
|
4990
|
+
if (images.length === 0) continue;
|
|
4991
|
+
return images.map((image, index) => ({
|
|
4992
|
+
label: `Image #${index + 1}`,
|
|
4993
|
+
uri: `attachment://${index + 1}`,
|
|
4994
|
+
image,
|
|
4995
|
+
}));
|
|
4996
|
+
}
|
|
4997
|
+
return [];
|
|
4998
|
+
}
|
|
4999
|
+
|
|
4941
5000
|
buildDisplaySessionContext(): SessionContext {
|
|
4942
5001
|
return deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
4943
5002
|
}
|
|
@@ -7729,7 +7788,17 @@ export class AgentSession {
|
|
|
7729
7788
|
await this.sessionManager.flush();
|
|
7730
7789
|
this.#cancelOwnAsyncJobs();
|
|
7731
7790
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
7791
|
+
// agent.reset() clears the core steering/follow-up queues. Preserve any queued
|
|
7792
|
+
// steers/follow-ups (RPC/SDK steer()/followUp() issued during the handoff, or a
|
|
7793
|
+
// pre-loader TUI steer) so they survive into the post-handoff session instead of
|
|
7794
|
+
// being silently dropped. Capture is synchronous immediately before reset and
|
|
7795
|
+
// restore is synchronous immediately after — no await gap — so a steer arriving
|
|
7796
|
+
// later (during ensureOnDisk/Bun.write below) appends to the restored queue
|
|
7797
|
+
// rather than being clobbered.
|
|
7798
|
+
const preservedSteering = this.agent.peekSteeringQueue().slice();
|
|
7799
|
+
const preservedFollowUp = this.agent.peekFollowUpQueue().slice();
|
|
7732
7800
|
this.agent.reset();
|
|
7801
|
+
this.agent.replaceQueues(preservedSteering, preservedFollowUp);
|
|
7733
7802
|
this.#freshProviderSessionId = undefined;
|
|
7734
7803
|
this.#syncAgentSessionId();
|
|
7735
7804
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -8785,14 +8854,20 @@ export class AgentSession {
|
|
|
8785
8854
|
const existingRoleValue = this.settings.getModelRole(role);
|
|
8786
8855
|
if (!existingRoleValue) return modelKey;
|
|
8787
8856
|
|
|
8788
|
-
const thinkingLevel = extractExplicitThinkingSelector(existingRoleValue, this.settings
|
|
8857
|
+
const thinkingLevel = extractExplicitThinkingSelector(existingRoleValue, this.settings, {
|
|
8858
|
+
isLiteralModelId: (provider, id) => this.#modelRegistry.find(provider, id) !== undefined,
|
|
8859
|
+
});
|
|
8789
8860
|
return formatModelSelectorValue(modelKey, thinkingLevel);
|
|
8790
8861
|
}
|
|
8791
8862
|
#resolveContextPromotionConfiguredTarget(currentModel: Model, availableModels: Model[]): Model | undefined {
|
|
8792
8863
|
const configuredTarget = currentModel.contextPromotionTarget?.trim();
|
|
8793
8864
|
if (!configuredTarget) return undefined;
|
|
8794
8865
|
|
|
8795
|
-
const parsed = parseModelString(configuredTarget
|
|
8866
|
+
const parsed = parseModelString(configuredTarget, {
|
|
8867
|
+
allowMaxAlias: true,
|
|
8868
|
+
isLiteralModelId: (provider, id) =>
|
|
8869
|
+
availableModels.some(model => model.provider === provider && model.id === id),
|
|
8870
|
+
});
|
|
8796
8871
|
if (parsed) {
|
|
8797
8872
|
const explicitModel = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
|
|
8798
8873
|
if (explicitModel) return explicitModel;
|
|
@@ -9087,7 +9162,6 @@ export class AgentSession {
|
|
|
9087
9162
|
);
|
|
9088
9163
|
}
|
|
9089
9164
|
}
|
|
9090
|
-
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
9091
9165
|
// Abort any older auto-compaction before installing this run's controller.
|
|
9092
9166
|
this.#autoCompactionAbortController?.abort();
|
|
9093
9167
|
const autoCompactionAbortController = new AbortController();
|
|
@@ -9095,11 +9169,16 @@ export class AgentSession {
|
|
|
9095
9169
|
const autoCompactionSignal = autoCompactionAbortController.signal;
|
|
9096
9170
|
|
|
9097
9171
|
try {
|
|
9172
|
+
// Emit start AFTER the controller is installed so isCompacting is already true
|
|
9173
|
+
// for any listener — and for input routed during this emit's event-loop yield:
|
|
9174
|
+
// a message typed as the compaction loader appears must land in the compaction
|
|
9175
|
+
// queue, not the core steering queue (which handoff's agent.reset() would wipe).
|
|
9176
|
+
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
9098
9177
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
9099
9178
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
9100
9179
|
const handoffResult = await this.handoff(handoffFocus, {
|
|
9101
9180
|
autoTriggered: true,
|
|
9102
|
-
signal:
|
|
9181
|
+
signal: autoCompactionSignal,
|
|
9103
9182
|
});
|
|
9104
9183
|
if (!handoffResult) {
|
|
9105
9184
|
const aborted = autoCompactionSignal.aborted;
|
|
@@ -9531,12 +9610,12 @@ export class AgentSession {
|
|
|
9531
9610
|
triggerContextTokens?: number,
|
|
9532
9611
|
): Promise<CompactionCheckResult | "fallback"> {
|
|
9533
9612
|
const action = "shake";
|
|
9534
|
-
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
9535
9613
|
this.#autoCompactionAbortController?.abort();
|
|
9536
9614
|
const controller = new AbortController();
|
|
9537
9615
|
this.#autoCompactionAbortController = controller;
|
|
9538
9616
|
const signal = controller.signal;
|
|
9539
9617
|
try {
|
|
9618
|
+
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
9540
9619
|
const result = await this.shake("elide", { config: DEFAULT_SHAKE_CONFIG, signal });
|
|
9541
9620
|
if (signal.aborted) {
|
|
9542
9621
|
await this.#emitSessionEvent({
|
|
@@ -9834,7 +9913,7 @@ export class AgentSession {
|
|
|
9834
9913
|
this.configWarnings.push(msg);
|
|
9835
9914
|
continue;
|
|
9836
9915
|
}
|
|
9837
|
-
const parsed = parseRetryFallbackSelector(selectorStr);
|
|
9916
|
+
const parsed = parseRetryFallbackSelector(selectorStr, this.#modelRegistry);
|
|
9838
9917
|
if (!parsed) {
|
|
9839
9918
|
const msg = `Invalid fallback selector format in role '${role}': ${selectorStr}`;
|
|
9840
9919
|
logger.warn(msg);
|
|
@@ -9857,7 +9936,7 @@ export class AgentSession {
|
|
|
9857
9936
|
|
|
9858
9937
|
#getRetryFallbackPrimarySelector(role: string): RetryFallbackSelector | undefined {
|
|
9859
9938
|
const configuredSelector = this.settings.getModelRole(role);
|
|
9860
|
-
return configuredSelector ? parseRetryFallbackSelector(configuredSelector) : undefined;
|
|
9939
|
+
return configuredSelector ? parseRetryFallbackSelector(configuredSelector, this.#modelRegistry) : undefined;
|
|
9861
9940
|
}
|
|
9862
9941
|
|
|
9863
9942
|
#clearActiveRetryFallback(): void {
|
|
@@ -9878,7 +9957,7 @@ export class AgentSession {
|
|
|
9878
9957
|
}
|
|
9879
9958
|
|
|
9880
9959
|
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
9881
|
-
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
9960
|
+
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
9882
9961
|
if (!parsedCurrent) return undefined;
|
|
9883
9962
|
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
9884
9963
|
const currentPlainSelector = this.model
|
|
@@ -9910,7 +9989,7 @@ export class AgentSession {
|
|
|
9910
9989
|
const chain = [primarySelector];
|
|
9911
9990
|
const seen = new Set<string>([primarySelector.raw]);
|
|
9912
9991
|
for (const selector of this.#getRetryFallbackChains()[role] ?? []) {
|
|
9913
|
-
const parsed = parseRetryFallbackSelector(selector);
|
|
9992
|
+
const parsed = parseRetryFallbackSelector(selector, this.#modelRegistry);
|
|
9914
9993
|
if (!parsed || seen.has(parsed.raw)) continue;
|
|
9915
9994
|
seen.add(parsed.raw);
|
|
9916
9995
|
chain.push(parsed);
|
|
@@ -9921,7 +10000,7 @@ export class AgentSession {
|
|
|
9921
10000
|
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
9922
10001
|
const chain = this.#getRetryFallbackEffectiveChain(role);
|
|
9923
10002
|
if (chain.length <= 1) return [];
|
|
9924
|
-
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
10003
|
+
const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
|
|
9925
10004
|
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
9926
10005
|
const currentPlainSelector =
|
|
9927
10006
|
this.model && parsedCurrent
|
|
@@ -10018,7 +10097,7 @@ export class AgentSession {
|
|
|
10018
10097
|
originalThinkingLevel,
|
|
10019
10098
|
lastAppliedFallbackThinkingLevel,
|
|
10020
10099
|
} = this.#activeRetryFallback;
|
|
10021
|
-
const originalSelector = parseRetryFallbackSelector(originalSelectorRaw);
|
|
10100
|
+
const originalSelector = parseRetryFallbackSelector(originalSelectorRaw, this.#modelRegistry);
|
|
10022
10101
|
if (!originalSelector) {
|
|
10023
10102
|
this.#clearActiveRetryFallback();
|
|
10024
10103
|
return;
|
|
@@ -11070,7 +11149,7 @@ export class AgentSession {
|
|
|
11070
11149
|
const hasServiceTierEntry = this.sessionManager
|
|
11071
11150
|
.getBranch()
|
|
11072
11151
|
.some(entry => entry.type === "service_tier_change");
|
|
11073
|
-
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
11152
|
+
const defaultThinkingLevel = parseConfiguredThinkingLevel(this.settings.get("defaultThinkingLevel"));
|
|
11074
11153
|
const configuredServiceTier = this.settings.get("serviceTier");
|
|
11075
11154
|
// Session log entries store only concrete levels. When `auto` has resolved
|
|
11076
11155
|
// for a turn, the persisted context may already carry that concrete level
|
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
5
5
|
import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
|
|
6
|
-
import type
|
|
6
|
+
import { type AutocompleteItem, Spacer } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
9
9
|
import { CollabHost } from "../collab/host";
|
|
@@ -30,6 +30,7 @@ import type { AgentSession, FreshSessionResult } from "../session/agent-session"
|
|
|
30
30
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
31
31
|
import { urlHyperlinkAlways } from "../tui";
|
|
32
32
|
import { getChangelogPath, parseChangelog } from "../utils/changelog";
|
|
33
|
+
import { CollabQrCodeComponent } from "./helpers/collab-qrcode";
|
|
33
34
|
import { buildContextReportText } from "./helpers/context-report";
|
|
34
35
|
import { formatDuration } from "./helpers/format";
|
|
35
36
|
import { createMarketplaceManager } from "./helpers/marketplace-manager";
|
|
@@ -99,6 +100,19 @@ function collabLinkHint(host: CollabHost, heading: string, view = false): string
|
|
|
99
100
|
].join("\n");
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
function showCollabQrCode(ctx: InteractiveModeContext, webLink: string): void {
|
|
104
|
+
try {
|
|
105
|
+
ctx.present([new Spacer(1), new CollabQrCodeComponent(webLink)]);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
ctx.showError(`Failed to render collab QR code: ${errorMessage(err)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function showCollabLink(ctx: InteractiveModeContext, host: CollabHost, heading: string, view = false): void {
|
|
112
|
+
ctx.showStatus(collabLinkHint(host, heading, view), { dim: false });
|
|
113
|
+
showCollabQrCode(ctx, view ? host.webViewLink : host.webLink);
|
|
114
|
+
}
|
|
115
|
+
|
|
102
116
|
function formatFreshSessionResult(result: FreshSessionResult): string {
|
|
103
117
|
const stateLabel = result.closedProviderSessions === 1 ? "provider state" : "provider states";
|
|
104
118
|
return `Fresh provider session started (${result.closedProviderSessions} ${stateLabel} pruned).`;
|
|
@@ -589,8 +603,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
589
603
|
const ctx = runtime.ctx;
|
|
590
604
|
ctx.editor.setText("");
|
|
591
605
|
const args = command.args.trim();
|
|
592
|
-
const
|
|
593
|
-
if (
|
|
606
|
+
const { verb, rest } = parseSubcommand(args);
|
|
607
|
+
if (verb === "stop") {
|
|
594
608
|
if (!ctx.collabHost) {
|
|
595
609
|
ctx.showStatus("Not hosting a collab session");
|
|
596
610
|
return;
|
|
@@ -599,7 +613,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
599
613
|
ctx.showStatus("Collab stopped");
|
|
600
614
|
return;
|
|
601
615
|
}
|
|
602
|
-
if (
|
|
616
|
+
if (verb === "status") {
|
|
603
617
|
if (ctx.collabHost) {
|
|
604
618
|
const names = ctx.collabHost.participants.map(p =>
|
|
605
619
|
p.role === "host" ? `${p.name} (host)` : p.readOnly ? `${p.name} (view-only)` : p.name,
|
|
@@ -620,15 +634,18 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
620
634
|
ctx.showError("Already in a collab session as a guest (/leave first)");
|
|
621
635
|
return;
|
|
622
636
|
}
|
|
623
|
-
const
|
|
637
|
+
const knownStartVerb = verb === "start" || verb === "view";
|
|
638
|
+
const view = verb === "view";
|
|
624
639
|
if (ctx.collabHost) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
640
|
+
showCollabLink(
|
|
641
|
+
ctx,
|
|
642
|
+
ctx.collabHost,
|
|
643
|
+
view ? "Read-only collab session active" : "Collab session active",
|
|
644
|
+
view,
|
|
628
645
|
);
|
|
629
646
|
return;
|
|
630
647
|
}
|
|
631
|
-
const explicitUrl =
|
|
648
|
+
const explicitUrl = knownStartVerb ? rest : args;
|
|
632
649
|
const relayInput = explicitUrl || ctx.settings.get("collab.relayUrl") || "";
|
|
633
650
|
if (!relayInput) {
|
|
634
651
|
ctx.showError(
|
|
@@ -638,15 +655,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
638
655
|
}
|
|
639
656
|
// Scheme-less relay args default to wss (ws:// must be spelled out for localhost).
|
|
640
657
|
const relayUrl = relayInput.includes("://") ? relayInput : `wss://${relayInput}`;
|
|
658
|
+
const webUrl = ctx.settings.get("collab.webUrl") || "";
|
|
641
659
|
const host = new CollabHost(ctx);
|
|
642
660
|
try {
|
|
643
|
-
await host.start(relayUrl);
|
|
661
|
+
await host.start(relayUrl, webUrl);
|
|
644
662
|
} catch (err) {
|
|
645
663
|
ctx.showError(`Failed to start collab session: ${errorMessage(err)}`);
|
|
646
664
|
return;
|
|
647
665
|
}
|
|
648
666
|
ctx.collabHost = host;
|
|
649
|
-
ctx
|
|
667
|
+
showCollabLink(ctx, host, "Collab session started!", view);
|
|
650
668
|
},
|
|
651
669
|
},
|
|
652
670
|
{
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Component, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { fgOrPlain } from "../../modes/theme/theme";
|
|
3
|
+
import { QrCode, renderQrHalfBlocks } from "../../utils/qrcode";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One-shot transcript block that prints a collab browser-join URL as a
|
|
7
|
+
* scannable QR code. The symbol is encoded once at construction (byte mode,
|
|
8
|
+
* EC level M) and rendered as ANSI half-blocks; on terminals too narrow for
|
|
9
|
+
* the symbol it degrades to a one-line hint pointing at the printed URL.
|
|
10
|
+
*/
|
|
11
|
+
export class CollabQrCodeComponent implements Component {
|
|
12
|
+
readonly #lines: readonly string[];
|
|
13
|
+
readonly #minWidth: number;
|
|
14
|
+
|
|
15
|
+
constructor(readonly url: string) {
|
|
16
|
+
const rows = renderQrHalfBlocks(QrCode.encodeText(url, "M"));
|
|
17
|
+
this.#lines = rows.map(row => ` ${row}`);
|
|
18
|
+
this.#minWidth = rows.reduce((max, row) => Math.max(max, visibleWidth(row)), 0) + 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render(width: number): readonly string[] {
|
|
22
|
+
if (width < this.#minWidth) {
|
|
23
|
+
const warning = `QR code hidden: terminal width ${width}; need ${this.#minWidth}. Use the browser URL above.`;
|
|
24
|
+
return [` ${fgOrPlain("warning", warning)}`];
|
|
25
|
+
}
|
|
26
|
+
return this.#lines;
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/thinking.ts
CHANGED
|
@@ -32,26 +32,45 @@ const THINKING_LEVEL_METADATA: Record<ThinkingLevel, ThinkingLevelMetadata> = {
|
|
|
32
32
|
[ThinkingLevel.High]: { value: ThinkingLevel.High, label: "high", description: "Deep reasoning (~16k tokens)" },
|
|
33
33
|
[ThinkingLevel.XHigh]: {
|
|
34
34
|
value: ThinkingLevel.XHigh,
|
|
35
|
-
label: "
|
|
35
|
+
label: "max",
|
|
36
36
|
description: "Maximum reasoning (~32k tokens)",
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
const EFFORT_BY_SELECTOR: Readonly<Record<string, Effort>> = {
|
|
41
|
+
[Effort.Minimal]: Effort.Minimal,
|
|
42
|
+
[Effort.Low]: Effort.Low,
|
|
43
|
+
[Effort.Medium]: Effort.Medium,
|
|
44
|
+
[Effort.High]: Effort.High,
|
|
45
|
+
[Effort.XHigh]: Effort.XHigh,
|
|
46
|
+
max: Effort.XHigh,
|
|
47
|
+
};
|
|
48
|
+
const THINKING_LEVEL_BY_SELECTOR: Readonly<Record<string, ThinkingLevel>> = {
|
|
49
|
+
[ThinkingLevel.Inherit]: ThinkingLevel.Inherit,
|
|
50
|
+
[ThinkingLevel.Off]: ThinkingLevel.Off,
|
|
51
|
+
[ThinkingLevel.Minimal]: ThinkingLevel.Minimal,
|
|
52
|
+
[ThinkingLevel.Low]: ThinkingLevel.Low,
|
|
53
|
+
[ThinkingLevel.Medium]: ThinkingLevel.Medium,
|
|
54
|
+
[ThinkingLevel.High]: ThinkingLevel.High,
|
|
55
|
+
[ThinkingLevel.XHigh]: ThinkingLevel.XHigh,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function getOwnSelector<T>(selectors: Readonly<Record<string, T>>, value: string | null | undefined): T | undefined {
|
|
59
|
+
return value === undefined || value === null || !Object.hasOwn(selectors, value) ? undefined : selectors[value];
|
|
60
|
+
}
|
|
42
61
|
|
|
43
62
|
/**
|
|
44
63
|
* Parses a provider-facing effort value.
|
|
45
64
|
*/
|
|
46
65
|
export function parseEffort(value: string | null | undefined): Effort | undefined {
|
|
47
|
-
return
|
|
66
|
+
return getOwnSelector(EFFORT_BY_SELECTOR, value);
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
/**
|
|
51
70
|
* Parses an agent-local thinking selector.
|
|
52
71
|
*/
|
|
53
72
|
export function parseThinkingLevel(value: string | null | undefined): ThinkingLevel | undefined {
|
|
54
|
-
return
|
|
73
|
+
return getOwnSelector(THINKING_LEVEL_BY_SELECTOR, value);
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
/**
|
|
@@ -125,6 +144,7 @@ const AUTO_THINKING_METADATA: ConfiguredThinkingLevelMetadata = {
|
|
|
125
144
|
*/
|
|
126
145
|
export function parseConfiguredThinkingLevel(value: string | null | undefined): ConfiguredThinkingLevel | undefined {
|
|
127
146
|
if (value === AUTO_THINKING) return AUTO_THINKING;
|
|
147
|
+
if (value === "max") return ThinkingLevel.XHigh;
|
|
128
148
|
return parseThinkingLevel(value);
|
|
129
149
|
}
|
|
130
150
|
|