@oh-my-pi/pi-coding-agent 14.8.1 → 14.9.1
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 +38 -0
- package/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +9 -9
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- package/src/edit/modes/hashline.ts +0 -2039
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.9.0] - 2026-05-10
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Moved hashline APIs to the dedicated `@oh-my-pi/pi-coding-agent/hashline` module, moved hash helpers to `@oh-my-pi/pi-coding-agent/hashline/hash`, and removed the legacy `edit/modes/hashline` and `edit/line-hash` source subpaths.
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- Removed hashline auto-rebase. Anchor mismatches now reject immediately so the model re-reads instead of silently relocating an edit to a hash-collision within ±5 lines, which could otherwise apply the change to the wrong region. Stale-anchor recovery via the cached read snapshot is unaffected.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fixed compaction crashing with `auth_unavailable` when the current model's provider has no credentials configured; compaction now falls back to an available model role (or fails fast with a clear error) instead of attempting a doomed provider call ([#986](https://github.com/can1357/oh-my-pi/issues/986)).
|
|
17
|
+
- Fixed top-level static import rewriting in JS evaluation to use parser-based detection so only real import declarations are rewritten and `import` text inside strings, comments, or template literals is preserved
|
|
18
|
+
- Fixed `import ... with` attribute handling in rewritten ESM imports so static imports with module attributes now become dynamic imports with matching `with` options
|
|
19
|
+
- Fixed model resolution silently falling back to a different provider (e.g. Amazon Bedrock) when `modelRoles` specified a fully-qualified `<provider>/<id>` whose exact pair was not in the bundled catalog. Explicit provider prefixes are now honored or surface a clear error ([#980](https://github.com/can1357/oh-my-pi/issues/980)).
|
|
20
|
+
- Fixed session count inflation on Anthropic backend caused by a fresh random `metadata.user_id` being generated on every API request; all requests within one conversation now share a stable `metadata.user_id` derived from the session ID, matching the expected one-session-per-conversation counting
|
|
21
|
+
- Fixed plan mode review resubmits to append each refreshed `local://PLAN.md` preview to the chat history, preserving the full refined plan in terminal scrollback.
|
|
22
|
+
- Fixed compaction requests (manual and auto) not carrying `metadata.user_id`, leaving them unattributed on the backend
|
|
23
|
+
- Fixed direct session-bound LLM calls (`/btw` ephemeral turns via `runEphemeralTurn`, branch summarization, session title generation) bypassing the agent and emitting a fresh random `metadata.user_id` per request on Anthropic OAuth: the session-level `prepareSimpleStreamOptions` helper now stamps the agent's session metadata onto direct calls, and `generateBranchSummary` plus `generateSessionTitle` accept and forward an explicit `metadata` option from the call site
|
|
24
|
+
- Fixed `metadata.user_id` lacking the authenticated `account_uuid` on Anthropic OAuth requests; sessions now install a dynamic resolver via `Agent#setMetadataResolver` that builds `{ session_id, account_uuid? }` per request, looking the live OAuth account UUID up from `AuthStorage` so it stays in sync with token refreshes and login/logout transitions instead of stranding a stale value
|
|
25
|
+
- Fixed multi-file legacy Pi extensions failing to load when sibling `.ts` files import each other via relative paths ([#983](https://github.com/can1357/oh-my-pi/issues/983)).
|
|
26
|
+
- Fixed sub-agent dispatch silently routing to a model whose provider has no working credentials (e.g. an unqualified `modelRoles.task` id like `qwen3.6-plus-free` resolving to a provider the user is not authenticated against). Task dispatch now falls back to the parent session's active model — which by definition has working auth — when the resolved subagent model has none ([#985](https://github.com/can1357/oh-my-pi/issues/985)).
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Added a debug-panel raw SSE stream viewer so stuck model/tool-call streams can be inspected live from the TUI.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Fixed legacy Pi plugin extensions failing to load on Windows when their entry path contains a drive letter ([#990](https://github.com/can1357/oh-my-pi/pull/990) by [@jiwangyihao](https://github.com/jiwangyihao)).
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Added `get_login_providers` RPC command to list registered OAuth providers with their current authentication status (`id`, `name`, `available`, `authenticated`)
|
|
39
|
+
- Added `login` RPC command to trigger OAuth login for a given provider; emits an `open_url` extension UI event (fire-and-forget) carrying the auth URL and optional instructions so headless clients can open the browser, then resolves when the callback-server flow completes
|
|
40
|
+
- Added `open_url` variant to `RpcExtensionUIRequest` for the above
|
|
41
|
+
- Added `getLoginProviders()` and `login(providerId)` methods to `RpcClient`
|
|
42
|
+
|
|
5
43
|
## [14.8.0] - 2026-05-09
|
|
6
44
|
### Added
|
|
7
45
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.
|
|
4
|
+
"version": "14.9.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -45,13 +45,14 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
|
+
"@babel/parser": "^7.29.3",
|
|
48
49
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.
|
|
50
|
+
"@oh-my-pi/omp-stats": "14.9.1",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "14.9.1",
|
|
52
|
+
"@oh-my-pi/pi-ai": "14.9.1",
|
|
53
|
+
"@oh-my-pi/pi-natives": "14.9.1",
|
|
54
|
+
"@oh-my-pi/pi-tui": "14.9.1",
|
|
55
|
+
"@oh-my-pi/pi-utils": "14.9.1",
|
|
55
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
57
|
"@sinclair/typebox": "^0.34.49",
|
|
57
58
|
"@types/turndown": "5.0.6",
|
|
@@ -227,6 +228,14 @@
|
|
|
227
228
|
"types": "./src/edit/modes/*.ts",
|
|
228
229
|
"import": "./src/edit/modes/*.ts"
|
|
229
230
|
},
|
|
231
|
+
"./hashline": {
|
|
232
|
+
"types": "./src/hashline/index.ts",
|
|
233
|
+
"import": "./src/hashline/index.ts"
|
|
234
|
+
},
|
|
235
|
+
"./hashline/*": {
|
|
236
|
+
"types": "./src/hashline/*.ts",
|
|
237
|
+
"import": "./src/hashline/*.ts"
|
|
238
|
+
},
|
|
230
239
|
"./exa": {
|
|
231
240
|
"types": "./src/exa/index.ts",
|
|
232
241
|
"import": "./src/exa/index.ts"
|
|
@@ -16,7 +16,7 @@ import chalk from "chalk";
|
|
|
16
16
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
17
17
|
import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
|
|
18
18
|
import { fuzzyMatch } from "../utils/fuzzy";
|
|
19
|
-
import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
19
|
+
import { isAuthenticated, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
20
20
|
import type { Settings } from "./settings";
|
|
21
21
|
|
|
22
22
|
/** Default model IDs for each known provider */
|
|
@@ -326,43 +326,49 @@ function tryMatchModel(
|
|
|
326
326
|
return exactCanonicalMatch;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
//
|
|
329
|
+
// Exact ID match (case-insensitive) — this must happen before provider-scoped
|
|
330
|
+
// fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
|
|
331
|
+
// IDs like "openai/gpt-4o:extended") still resolve as IDs instead of being
|
|
332
|
+
// misread as a provider-qualified selector.
|
|
333
|
+
const exactMatches = availableModels.filter(m => m.id.toLowerCase() === modelPattern.toLowerCase());
|
|
334
|
+
if (exactMatches.length > 0) {
|
|
335
|
+
return pickPreferredModel(exactMatches, context);
|
|
336
|
+
}
|
|
337
|
+
// Check for provider/modelId format — fuzzy match within provider only.
|
|
330
338
|
const slashIndex = modelPattern.indexOf("/");
|
|
331
339
|
if (slashIndex !== -1) {
|
|
332
340
|
const provider = modelPattern.substring(0, slashIndex);
|
|
333
341
|
const modelId = modelPattern.substring(slashIndex + 1);
|
|
334
|
-
|
|
335
342
|
const providerModels = availableModels.filter(m => m.provider.toLowerCase() === provider.toLowerCase());
|
|
336
|
-
if (providerModels.length
|
|
343
|
+
if (providerModels.length === 0) {
|
|
344
|
+
// The prefix is not a known provider in this candidate set, so treat the
|
|
345
|
+
// slash as part of the raw model ID and continue with generic matching.
|
|
346
|
+
} else {
|
|
337
347
|
const scored = providerModels
|
|
338
348
|
.map(model => ({ model, match: fuzzyMatch(modelId, model.id) }))
|
|
339
349
|
.filter(entry => entry.match.matches);
|
|
340
|
-
if (scored.length
|
|
341
|
-
|
|
342
|
-
if (a.match.score !== b.match.score) return a.match.score - b.match.score;
|
|
343
|
-
const aKey = formatModelString(a.model);
|
|
344
|
-
const bKey = formatModelString(b.model);
|
|
345
|
-
const aUsage = context.modelUsageRank.get(aKey) ?? Number.POSITIVE_INFINITY;
|
|
346
|
-
const bUsage = context.modelUsageRank.get(bKey) ?? Number.POSITIVE_INFINITY;
|
|
347
|
-
if (aUsage !== bUsage) return aUsage - bUsage;
|
|
348
|
-
|
|
349
|
-
const aProviderUsage = context.providerUsageRank.get(a.model.provider) ?? Number.POSITIVE_INFINITY;
|
|
350
|
-
const bProviderUsage = context.providerUsageRank.get(b.model.provider) ?? Number.POSITIVE_INFINITY;
|
|
351
|
-
if (aProviderUsage !== bProviderUsage) return aProviderUsage - bProviderUsage;
|
|
352
|
-
|
|
353
|
-
const aOrder = context.modelOrder.get(aKey) ?? 0;
|
|
354
|
-
const bOrder = context.modelOrder.get(bKey) ?? 0;
|
|
355
|
-
return aOrder - bOrder;
|
|
356
|
-
});
|
|
357
|
-
return scored[0]?.model;
|
|
350
|
+
if (scored.length === 0) {
|
|
351
|
+
return undefined;
|
|
358
352
|
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
353
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
354
|
+
scored.sort((a, b) => {
|
|
355
|
+
if (a.match.score !== b.match.score) return a.match.score - b.match.score;
|
|
356
|
+
const aKey = formatModelString(a.model);
|
|
357
|
+
const bKey = formatModelString(b.model);
|
|
358
|
+
const aUsage = context.modelUsageRank.get(aKey) ?? Number.POSITIVE_INFINITY;
|
|
359
|
+
const bUsage = context.modelUsageRank.get(bKey) ?? Number.POSITIVE_INFINITY;
|
|
360
|
+
if (aUsage !== bUsage) return aUsage - bUsage;
|
|
361
|
+
|
|
362
|
+
const aProviderUsage = context.providerUsageRank.get(a.model.provider) ?? Number.POSITIVE_INFINITY;
|
|
363
|
+
const bProviderUsage = context.providerUsageRank.get(b.model.provider) ?? Number.POSITIVE_INFINITY;
|
|
364
|
+
if (aProviderUsage !== bProviderUsage) return aProviderUsage - bProviderUsage;
|
|
365
|
+
|
|
366
|
+
const aOrder = context.modelOrder.get(aKey) ?? 0;
|
|
367
|
+
const bOrder = context.modelOrder.get(bKey) ?? 0;
|
|
368
|
+
return aOrder - bOrder;
|
|
369
|
+
});
|
|
370
|
+
return scored[0]?.model;
|
|
371
|
+
}
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
// No exact match - fall back to partial matching
|
|
@@ -688,18 +694,18 @@ export function resolveModelFromSettings(options: {
|
|
|
688
694
|
}): Model<Api> | undefined {
|
|
689
695
|
const { settings, availableModels, matchPreferences, roleOrder, modelRegistry } = options;
|
|
690
696
|
const roles = roleOrder ?? MODEL_ROLE_IDS;
|
|
697
|
+
let sawConfiguredProviderQualifiedRole = false;
|
|
691
698
|
for (const role of roles) {
|
|
692
699
|
const configured = settings.getModelRole(role);
|
|
693
700
|
if (!configured) continue;
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
);
|
|
701
|
+
const expanded = expandRoleAlias(configured, settings).trim();
|
|
702
|
+
if (expanded.includes("/")) {
|
|
703
|
+
sawConfiguredProviderQualifiedRole = true;
|
|
704
|
+
}
|
|
705
|
+
const resolved = resolveModelFromString(expanded, availableModels, matchPreferences, modelRegistry);
|
|
700
706
|
if (resolved) return resolved;
|
|
701
707
|
}
|
|
702
|
-
return availableModels[0];
|
|
708
|
+
return sawConfiguredProviderQualifiedRole ? undefined : availableModels[0];
|
|
703
709
|
}
|
|
704
710
|
|
|
705
711
|
/**
|
|
@@ -726,6 +732,57 @@ export function resolveModelOverride(
|
|
|
726
732
|
return { explicitThinkingLevel: false };
|
|
727
733
|
}
|
|
728
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Resolve a list of override patterns to the first matching model, with an
|
|
737
|
+
* auth-aware fallback to the parent session's active model.
|
|
738
|
+
*
|
|
739
|
+
* If the resolved subagent model has no working credentials (provider has no
|
|
740
|
+
* usable auth), and the parent's active model resolves with working auth,
|
|
741
|
+
* use the parent's model instead. This prevents subagent dispatch from
|
|
742
|
+
* silently routing to a provider the user can't actually call (e.g.
|
|
743
|
+
* `modelRoles.task` pointing at an unqualified id whose only available
|
|
744
|
+
* provider variant has no configured credentials — see #985).
|
|
745
|
+
*
|
|
746
|
+
* If neither the subagent nor the parent has working auth, returns the
|
|
747
|
+
* primary resolution unchanged so the existing error path still surfaces
|
|
748
|
+
* a meaningful failure downstream.
|
|
749
|
+
*/
|
|
750
|
+
export async function resolveModelOverrideWithAuthFallback(
|
|
751
|
+
modelPatterns: string[],
|
|
752
|
+
parentActiveModelPattern: string | undefined,
|
|
753
|
+
modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
|
|
754
|
+
settings?: Settings,
|
|
755
|
+
): Promise<{
|
|
756
|
+
model?: Model<Api>;
|
|
757
|
+
thinkingLevel?: ThinkingLevel;
|
|
758
|
+
explicitThinkingLevel: boolean;
|
|
759
|
+
authFallbackUsed: boolean;
|
|
760
|
+
}> {
|
|
761
|
+
const primary = resolveModelOverride(modelPatterns, modelRegistry, settings);
|
|
762
|
+
if (!primary.model || !parentActiveModelPattern) {
|
|
763
|
+
return { ...primary, authFallbackUsed: false };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const primaryKey = await modelRegistry.getApiKey(primary.model);
|
|
767
|
+
if (isAuthenticated(primaryKey)) {
|
|
768
|
+
return { ...primary, authFallbackUsed: false };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const fallback = resolveModelOverride([parentActiveModelPattern], modelRegistry, settings);
|
|
772
|
+
if (!fallback.model) {
|
|
773
|
+
return { ...primary, authFallbackUsed: false };
|
|
774
|
+
}
|
|
775
|
+
if (modelsAreEqual(fallback.model, primary.model)) {
|
|
776
|
+
return { ...primary, authFallbackUsed: false };
|
|
777
|
+
}
|
|
778
|
+
const fallbackKey = await modelRegistry.getApiKey(fallback.model);
|
|
779
|
+
if (!isAuthenticated(fallbackKey)) {
|
|
780
|
+
return { ...primary, authFallbackUsed: false };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return { ...fallback, authFallbackUsed: true };
|
|
784
|
+
}
|
|
785
|
+
|
|
729
786
|
/**
|
|
730
787
|
* Resolve a list of role patterns to the first matching model.
|
|
731
788
|
*/
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
parseFrontmatter,
|
|
9
9
|
prompt,
|
|
10
10
|
} from "@oh-my-pi/pi-utils";
|
|
11
|
-
import { computeLineHash, HL_BODY_SEP, HL_EDIT_SEP } from "../
|
|
11
|
+
import { computeLineHash, HL_BODY_SEP, HL_EDIT_SEP } from "../hashline/hash";
|
|
12
12
|
import { jtdToTypeScript } from "../tools/jtd-to-typescript";
|
|
13
13
|
import { parseCommandArgs, substituteArgs } from "../utils/command-args";
|
|
14
14
|
|
package/src/debug/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { formatBytes } from "../tools/render-utils";
|
|
|
15
15
|
import { openPath } from "../utils/open";
|
|
16
16
|
import { DebugLogViewerComponent } from "./log-viewer";
|
|
17
17
|
import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from "./profiler";
|
|
18
|
+
import { RawSseViewerComponent } from "./raw-sse";
|
|
19
|
+
import { resolveRawSseDebugBuffer } from "./raw-sse-buffer";
|
|
18
20
|
import { clearArtifactCache, createDebugLogSource, createReportBundle, getArtifactCacheStats } from "./report-bundle";
|
|
19
21
|
import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
|
20
22
|
|
|
@@ -27,6 +29,7 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
|
27
29
|
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
28
30
|
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
29
31
|
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
32
|
+
{ value: "raw-sse", label: "View: raw SSE stream", description: "Show live provider SSE frames" },
|
|
30
33
|
{
|
|
31
34
|
value: "transcript",
|
|
32
35
|
label: "Export: TUI transcript",
|
|
@@ -97,6 +100,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
97
100
|
case "logs":
|
|
98
101
|
await this.#handleViewLogs();
|
|
99
102
|
break;
|
|
103
|
+
case "raw-sse":
|
|
104
|
+
await this.#handleViewRawSse();
|
|
105
|
+
break;
|
|
100
106
|
case "system":
|
|
101
107
|
await this.#handleViewSystemInfo();
|
|
102
108
|
break;
|
|
@@ -315,6 +321,21 @@ export class DebugSelectorComponent extends Container {
|
|
|
315
321
|
this.ctx.ui.requestRender();
|
|
316
322
|
}
|
|
317
323
|
|
|
324
|
+
async #handleViewRawSse(): Promise<void> {
|
|
325
|
+
const viewer = new RawSseViewerComponent({
|
|
326
|
+
buffer: resolveRawSseDebugBuffer(this.ctx.session),
|
|
327
|
+
terminalRows: this.ctx.ui.terminal.rows,
|
|
328
|
+
onExit: () => this.ctx.showDebugSelector(),
|
|
329
|
+
onStatus: message => this.ctx.showStatus(message, { dim: true }),
|
|
330
|
+
onUpdate: () => this.ctx.ui.requestRender(),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
this.ctx.editorContainer.clear();
|
|
334
|
+
this.ctx.editorContainer.addChild(viewer);
|
|
335
|
+
this.ctx.ui.setFocus(viewer);
|
|
336
|
+
this.ctx.ui.requestRender();
|
|
337
|
+
}
|
|
338
|
+
|
|
318
339
|
async #handleViewSystemInfo(): Promise<void> {
|
|
319
340
|
try {
|
|
320
341
|
const info = await collectSystemInfo();
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Model, ProviderResponseMetadata, RawSseEvent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
|
|
3
|
+
const MAX_RAW_SSE_EVENTS = 1_000;
|
|
4
|
+
const MAX_RAW_SSE_CHARS = 512_000;
|
|
5
|
+
const MAX_RAW_SSE_EVENT_CHARS = 64_000;
|
|
6
|
+
|
|
7
|
+
export type RawSseDebugRecord =
|
|
8
|
+
| {
|
|
9
|
+
kind: "response";
|
|
10
|
+
sequence: number;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
provider?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
api?: string;
|
|
15
|
+
status: number;
|
|
16
|
+
requestId?: string | null;
|
|
17
|
+
transport?: string;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
kind: "event";
|
|
21
|
+
sequence: number;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
provider?: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
api?: string;
|
|
26
|
+
event: string | null;
|
|
27
|
+
raw: string[];
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
originalChars: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface RawSseDebugSnapshot {
|
|
33
|
+
records: readonly RawSseDebugRecord[];
|
|
34
|
+
droppedRecords: number;
|
|
35
|
+
droppedChars: number;
|
|
36
|
+
totalEvents: number;
|
|
37
|
+
lastUpdatedAt?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function modelProvider(model: Model | undefined): string | undefined {
|
|
41
|
+
return model?.provider;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function modelId(model: Model | undefined): string | undefined {
|
|
45
|
+
return model?.id;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function modelApi(model: Model | undefined): string | undefined {
|
|
49
|
+
return model?.api;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function countRecordChars(record: RawSseDebugRecord): number {
|
|
53
|
+
if (record.kind === "response") return formatRawSseResponseComment(record).length + 1;
|
|
54
|
+
return record.raw.reduce((sum, line) => sum + line.length + 1, 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function trimRawLines(raw: string[]): { raw: string[]; truncated: boolean; originalChars: number } {
|
|
58
|
+
const originalChars = raw.reduce((sum, line) => sum + line.length + 1, 0);
|
|
59
|
+
if (originalChars <= MAX_RAW_SSE_EVENT_CHARS) {
|
|
60
|
+
return { raw: [...raw], truncated: false, originalChars };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const trimmed: string[] = [];
|
|
64
|
+
let remaining = MAX_RAW_SSE_EVENT_CHARS;
|
|
65
|
+
for (const line of raw) {
|
|
66
|
+
if (remaining <= 0) break;
|
|
67
|
+
if (line.length + 1 <= remaining) {
|
|
68
|
+
trimmed.push(line);
|
|
69
|
+
remaining -= line.length + 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
trimmed.push(line.slice(0, Math.max(0, remaining)));
|
|
73
|
+
remaining = 0;
|
|
74
|
+
}
|
|
75
|
+
trimmed.push(`: omp-debug-truncated originalChars=${originalChars}`);
|
|
76
|
+
return { raw: trimmed, truncated: true, originalChars };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatRawSseIsoTime(timestamp: number): string {
|
|
80
|
+
return new Date(timestamp).toISOString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatRawSseResponseComment(record: Extract<RawSseDebugRecord, { kind: "response" }>): string {
|
|
84
|
+
const fields = [
|
|
85
|
+
"omp-response",
|
|
86
|
+
`ts=${formatRawSseIsoTime(record.timestamp)}`,
|
|
87
|
+
`status=${record.status}`,
|
|
88
|
+
record.provider ? `provider=${record.provider}` : undefined,
|
|
89
|
+
record.model ? `model=${record.model}` : undefined,
|
|
90
|
+
record.api ? `api=${record.api}` : undefined,
|
|
91
|
+
record.requestId ? `requestId=${record.requestId}` : undefined,
|
|
92
|
+
record.transport ? `transport=${record.transport}` : undefined,
|
|
93
|
+
].filter((field): field is string => field !== undefined);
|
|
94
|
+
return `: ${fields.join(" ")}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function rawSseRecordLines(record: RawSseDebugRecord): string[] {
|
|
98
|
+
if (record.kind === "response") return [formatRawSseResponseComment(record)];
|
|
99
|
+
return record.raw;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function rawRecordText(record: RawSseDebugRecord): string {
|
|
103
|
+
return `${rawSseRecordLines(record).join("\n")}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function metadataTransport(response: ProviderResponseMetadata): string | undefined {
|
|
107
|
+
const value = response.metadata?.lastTransport;
|
|
108
|
+
return typeof value === "string" ? value : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class RawSseDebugBuffer {
|
|
112
|
+
#records: RawSseDebugRecord[] = [];
|
|
113
|
+
#totalChars = 0;
|
|
114
|
+
#droppedRecords = 0;
|
|
115
|
+
#droppedChars = 0;
|
|
116
|
+
#totalEvents = 0;
|
|
117
|
+
#lastUpdatedAt: number | undefined;
|
|
118
|
+
#nextSequence = 1;
|
|
119
|
+
#listeners = new Set<() => void>();
|
|
120
|
+
|
|
121
|
+
subscribe(listener: () => void): () => void {
|
|
122
|
+
this.#listeners.add(listener);
|
|
123
|
+
return () => this.#listeners.delete(listener);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
recordResponse(response: ProviderResponseMetadata, model?: Model): void {
|
|
127
|
+
this.#append({
|
|
128
|
+
kind: "response",
|
|
129
|
+
sequence: this.#nextSequence++,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
provider: modelProvider(model),
|
|
132
|
+
model: modelId(model),
|
|
133
|
+
api: modelApi(model),
|
|
134
|
+
status: response.status,
|
|
135
|
+
requestId: response.requestId,
|
|
136
|
+
transport: metadataTransport(response),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
recordEvent(event: RawSseEvent, model?: Model): void {
|
|
141
|
+
const trimmed = trimRawLines(event.raw);
|
|
142
|
+
this.#totalEvents += 1;
|
|
143
|
+
this.#append({
|
|
144
|
+
kind: "event",
|
|
145
|
+
sequence: this.#nextSequence++,
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
provider: modelProvider(model),
|
|
148
|
+
model: modelId(model),
|
|
149
|
+
api: modelApi(model),
|
|
150
|
+
event: event.event,
|
|
151
|
+
raw: trimmed.raw,
|
|
152
|
+
truncated: trimmed.truncated,
|
|
153
|
+
originalChars: trimmed.originalChars,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
snapshot(): RawSseDebugSnapshot {
|
|
158
|
+
return {
|
|
159
|
+
records: [...this.#records],
|
|
160
|
+
droppedRecords: this.#droppedRecords,
|
|
161
|
+
droppedChars: this.#droppedChars,
|
|
162
|
+
totalEvents: this.#totalEvents,
|
|
163
|
+
lastUpdatedAt: this.#lastUpdatedAt,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
toRawText(): string {
|
|
168
|
+
return this.#records.map(rawRecordText).join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#append(record: RawSseDebugRecord): void {
|
|
172
|
+
const chars = countRecordChars(record);
|
|
173
|
+
this.#records.push(record);
|
|
174
|
+
this.#totalChars += chars;
|
|
175
|
+
this.#lastUpdatedAt = record.timestamp;
|
|
176
|
+
this.#enforceLimits();
|
|
177
|
+
this.#emit();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#enforceLimits(): void {
|
|
181
|
+
while (this.#records.length > MAX_RAW_SSE_EVENTS || this.#totalChars > MAX_RAW_SSE_CHARS) {
|
|
182
|
+
const dropped = this.#records.shift();
|
|
183
|
+
if (!dropped) return;
|
|
184
|
+
const chars = countRecordChars(dropped);
|
|
185
|
+
this.#totalChars = Math.max(0, this.#totalChars - chars);
|
|
186
|
+
this.#droppedRecords += 1;
|
|
187
|
+
this.#droppedChars += chars;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#emit(): void {
|
|
192
|
+
for (const listener of this.#listeners) {
|
|
193
|
+
try {
|
|
194
|
+
listener();
|
|
195
|
+
} catch {
|
|
196
|
+
// Debug viewers must not be able to break stream capture.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const fallbackBuffers = new WeakMap<object, RawSseDebugBuffer>();
|
|
203
|
+
const globalFallbackBuffer = new RawSseDebugBuffer();
|
|
204
|
+
|
|
205
|
+
export function resolveRawSseDebugBuffer(owner?: object): RawSseDebugBuffer {
|
|
206
|
+
if (!owner) return globalFallbackBuffer;
|
|
207
|
+
|
|
208
|
+
const candidate = (owner as { rawSseDebugBuffer?: unknown }).rawSseDebugBuffer;
|
|
209
|
+
if (candidate instanceof RawSseDebugBuffer) return candidate;
|
|
210
|
+
|
|
211
|
+
const existing = fallbackBuffers.get(owner);
|
|
212
|
+
if (existing) return existing;
|
|
213
|
+
|
|
214
|
+
const buffer = new RawSseDebugBuffer();
|
|
215
|
+
fallbackBuffers.set(owner, buffer);
|
|
216
|
+
if (Object.isExtensible(owner)) {
|
|
217
|
+
try {
|
|
218
|
+
Object.defineProperty(owner, "rawSseDebugBuffer", {
|
|
219
|
+
value: buffer,
|
|
220
|
+
configurable: true,
|
|
221
|
+
enumerable: false,
|
|
222
|
+
writable: true,
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
// The WeakMap fallback remains usable if the session object rejects extension.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return buffer;
|
|
229
|
+
}
|