gsd-pi 2.34.0-dev.7d38042 → 2.34.0-dev.e6d9bed
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/dist/resources/extensions/gsd/commands-prefs-wizard.js +5 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +10 -0
- package/dist/resources/extensions/gsd/doctor-checks.js +113 -5
- package/dist/resources/extensions/gsd/doctor-proactive.js +22 -0
- package/dist/resources/extensions/gsd/doctor.js +36 -0
- package/dist/resources/extensions/gsd/guided-flow.js +4 -2
- package/dist/resources/extensions/gsd/preferences-validation.js +38 -0
- package/dist/resources/extensions/gsd/preferences.js +2 -0
- package/dist/resources/skills/create-gsd-extension/references/events-reference.md +4 -4
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts +14 -0
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +24 -27
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +1 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +11 -22
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/proxy.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/proxy.js +2 -8
- package/packages/pi-agent-core/dist/proxy.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +30 -27
- package/packages/pi-agent-core/src/agent.ts +12 -23
- package/packages/pi-agent-core/src/proxy.ts +2 -8
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +5 -41
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +10 -73
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +8 -79
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-shared.d.ts +65 -0
- package/packages/pi-ai/dist/providers/openai-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/openai-shared.js +146 -0
- package/packages/pi-ai/dist/providers/openai-shared.js.map +1 -0
- package/packages/pi-ai/dist/utils/oauth/google-antigravity.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-antigravity.js +7 -135
- package/packages/pi-ai/dist/utils/oauth/google-antigravity.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js +7 -135
- package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/utils/oauth/google-oauth-utils.d.ts +46 -0
- package/packages/pi-ai/dist/utils/oauth/google-oauth-utils.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/oauth/google-oauth-utils.js +160 -0
- package/packages/pi-ai/dist/utils/oauth/google-oauth-utils.js.map +1 -0
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +11 -45
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -86
- package/packages/pi-ai/src/providers/openai-responses.ts +15 -95
- package/packages/pi-ai/src/providers/openai-shared.ts +193 -0
- package/packages/pi-ai/src/utils/oauth/google-antigravity.ts +14 -162
- package/packages/pi-ai/src/utils/oauth/google-gemini-cli.ts +13 -161
- package/packages/pi-ai/src/utils/oauth/google-oauth-utils.ts +201 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +16 -63
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +104 -641
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +0 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +4 -35
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js +5 -43
- package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js +11 -69
- package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.d.ts +40 -0
- package/packages/pi-coding-agent/dist/core/compaction/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +78 -0
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts +77 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +331 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/index.js +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +129 -243
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +49 -42
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js +2 -21
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lock-utils.d.ts +39 -0
- package/packages/pi-coding-agent/dist/core/lock-utils.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lock-utils.js +89 -0
- package/packages/pi-coding-agent/dist/core/lock-utils.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js +4 -1
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +52 -107
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +2 -21
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +0 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +0 -28
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-manager.js +2 -4
- package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +2 -4
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +33 -58
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +87 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.js +295 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +0 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -28
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +1 -3
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js +9 -26
- package/packages/pi-coding-agent/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +1 -13
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-render-utils.d.ts +44 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-render-utils.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-render-utils.js +61 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-render-utils.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-selector.js +6 -9
- package/packages/pi-coding-agent/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/utils/shorten-path.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/utils/shorten-path.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/utils/shorten-path.js +15 -0
- package/packages/pi-coding-agent/dist/modes/interactive/utils/shorten-path.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/print-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/print-mode.js +2 -30
- package/packages/pi-coding-agent/dist/modes/print-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +2 -28
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/shared/command-context-actions.d.ts +19 -0
- package/packages/pi-coding-agent/dist/modes/shared/command-context-actions.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/shared/command-context-actions.js +45 -0
- package/packages/pi-coding-agent/dist/modes/shared/command-context-actions.js.map +1 -0
- package/packages/pi-coding-agent/dist/utils/error.d.ts +5 -0
- package/packages/pi-coding-agent/dist/utils/error.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/utils/error.js +7 -0
- package/packages/pi-coding-agent/dist/utils/error.js.map +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +117 -745
- package/packages/pi-coding-agent/src/core/auth-storage.ts +4 -38
- package/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +7 -53
- package/packages/pi-coding-agent/src/core/compaction/compaction.ts +14 -74
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +100 -0
- package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +424 -0
- package/packages/pi-coding-agent/src/core/extensions/index.ts +1 -21
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +119 -243
- package/packages/pi-coding-agent/src/core/extensions/types.ts +50 -69
- package/packages/pi-coding-agent/src/core/lock-utils.ts +113 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +4 -1
- package/packages/pi-coding-agent/src/core/lsp/index.ts +83 -152
- package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +2 -22
- package/packages/pi-coding-agent/src/core/lsp/types.ts +0 -29
- package/packages/pi-coding-agent/src/core/package-manager.ts +1 -4
- package/packages/pi-coding-agent/src/core/resource-loader.ts +43 -67
- package/packages/pi-coding-agent/src/core/retry-handler.ts +359 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +3 -30
- package/packages/pi-coding-agent/src/core/skills.ts +1 -4
- package/packages/pi-coding-agent/src/index.ts +1 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts +17 -29
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +1 -13
- package/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts +81 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts +14 -19
- package/packages/pi-coding-agent/src/modes/interactive/utils/shorten-path.ts +14 -0
- package/packages/pi-coding-agent/src/modes/print-mode.ts +2 -30
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -28
- package/packages/pi-coding-agent/src/modes/shared/command-context-actions.ts +53 -0
- package/packages/pi-coding-agent/src/utils/error.ts +6 -0
- package/packages/pi-tui/dist/components/markdown.d.ts +5 -0
- package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/markdown.js +25 -31
- package/packages/pi-tui/dist/components/markdown.js.map +1 -1
- package/packages/pi-tui/dist/keys.d.ts +0 -4
- package/packages/pi-tui/dist/keys.d.ts.map +1 -1
- package/packages/pi-tui/dist/keys.js +94 -162
- package/packages/pi-tui/dist/keys.js.map +1 -1
- package/packages/pi-tui/src/components/markdown.ts +25 -29
- package/packages/pi-tui/src/keys.ts +94 -173
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +5 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +10 -0
- package/src/resources/extensions/gsd/doctor-checks.ts +107 -5
- package/src/resources/extensions/gsd/doctor-proactive.ts +24 -0
- package/src/resources/extensions/gsd/doctor-types.ts +9 -1
- package/src/resources/extensions/gsd/doctor.ts +35 -0
- package/src/resources/extensions/gsd/guided-flow.ts +4 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +38 -0
- package/src/resources/extensions/gsd/preferences.ts +2 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +98 -2
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +59 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +28 -0
- package/src/resources/skills/create-gsd-extension/references/events-reference.md +4 -4
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RetryHandler - Automatic retry logic with exponential backoff and credential/provider fallback.
|
|
3
|
+
*
|
|
4
|
+
* Handles retryable errors (overloaded, rate limit, server errors) by:
|
|
5
|
+
* 1. Trying alternate credentials for the same provider
|
|
6
|
+
* 2. Falling back to other providers via FallbackResolver
|
|
7
|
+
* 3. Exponential backoff with configurable max retries
|
|
8
|
+
*
|
|
9
|
+
* Context overflow errors are NOT handled here (see compaction).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Agent } from "@gsd/pi-agent-core";
|
|
13
|
+
import type { AssistantMessage, Model } from "@gsd/pi-ai";
|
|
14
|
+
import { isContextOverflow } from "@gsd/pi-ai";
|
|
15
|
+
import type { UsageLimitErrorType } from "./auth-storage.js";
|
|
16
|
+
import type { FallbackResolver } from "./fallback-resolver.js";
|
|
17
|
+
import type { ModelRegistry } from "./model-registry.js";
|
|
18
|
+
import type { SettingsManager } from "./settings-manager.js";
|
|
19
|
+
import { sleep } from "../utils/sleep.js";
|
|
20
|
+
import type { AgentSessionEvent } from "./agent-session.js";
|
|
21
|
+
|
|
22
|
+
/** Dependencies injected from AgentSession into RetryHandler */
|
|
23
|
+
export interface RetryHandlerDeps {
|
|
24
|
+
readonly agent: Agent;
|
|
25
|
+
readonly settingsManager: SettingsManager;
|
|
26
|
+
readonly modelRegistry: ModelRegistry;
|
|
27
|
+
readonly fallbackResolver: FallbackResolver;
|
|
28
|
+
getModel: () => Model<any> | undefined;
|
|
29
|
+
getSessionId: () => string;
|
|
30
|
+
emit: (event: AgentSessionEvent) => void;
|
|
31
|
+
/** Called when the retry handler switches to a fallback model */
|
|
32
|
+
onModelChange: (model: Model<any>) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RetryHandler {
|
|
36
|
+
private _retryAbortController: AbortController | undefined = undefined;
|
|
37
|
+
private _retryAttempt = 0;
|
|
38
|
+
private _retryPromise: Promise<void> | undefined = undefined;
|
|
39
|
+
private _retryResolve: (() => void) | undefined = undefined;
|
|
40
|
+
|
|
41
|
+
constructor(private readonly _deps: RetryHandlerDeps) {}
|
|
42
|
+
|
|
43
|
+
/** Current retry attempt (0 if not retrying) */
|
|
44
|
+
get retryAttempt(): number {
|
|
45
|
+
return this._retryAttempt;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whether auto-retry is currently in progress */
|
|
49
|
+
get isRetrying(): boolean {
|
|
50
|
+
return this._retryPromise !== undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether auto-retry is enabled */
|
|
54
|
+
get autoRetryEnabled(): boolean {
|
|
55
|
+
return this._deps.settingsManager.getRetryEnabled();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Toggle auto-retry setting */
|
|
59
|
+
setAutoRetryEnabled(enabled: boolean): void {
|
|
60
|
+
this._deps.settingsManager.setRetryEnabled(enabled);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a retry promise synchronously for agent_end events.
|
|
65
|
+
* Must be called synchronously from the agent event handler before
|
|
66
|
+
* any async processing, so that waitForRetry() doesn't miss in-flight retries.
|
|
67
|
+
*/
|
|
68
|
+
createRetryPromiseForAgentEnd(messages: Array<{ role: string } & Record<string, any>>): void {
|
|
69
|
+
if (this._retryPromise) return;
|
|
70
|
+
|
|
71
|
+
const settings = this._deps.settingsManager.getRetrySettings();
|
|
72
|
+
if (!settings.enabled) return;
|
|
73
|
+
|
|
74
|
+
const lastAssistant = this._findLastAssistantInMessages(messages);
|
|
75
|
+
if (!lastAssistant || !this.isRetryableError(lastAssistant)) return;
|
|
76
|
+
|
|
77
|
+
this._retryPromise = new Promise((resolve) => {
|
|
78
|
+
this._retryResolve = resolve;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle a successful assistant response by resetting retry state.
|
|
84
|
+
* Call this when an assistant message completes without error.
|
|
85
|
+
*/
|
|
86
|
+
handleSuccessfulResponse(): void {
|
|
87
|
+
if (this._retryAttempt > 0) {
|
|
88
|
+
this._deps.emit({
|
|
89
|
+
type: "auto_retry_end",
|
|
90
|
+
success: true,
|
|
91
|
+
attempt: this._retryAttempt,
|
|
92
|
+
});
|
|
93
|
+
this._retryAttempt = 0;
|
|
94
|
+
this._resolveRetry();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if an error is retryable (overloaded, rate limit, server errors).
|
|
100
|
+
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
101
|
+
*/
|
|
102
|
+
isRetryableError(message: AssistantMessage): boolean {
|
|
103
|
+
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
104
|
+
|
|
105
|
+
// Context overflow is handled by compaction, not retry
|
|
106
|
+
const contextWindow = this._deps.getModel()?.contextWindow ?? 0;
|
|
107
|
+
if (isContextOverflow(message, contextWindow)) return false;
|
|
108
|
+
|
|
109
|
+
const err = message.errorMessage;
|
|
110
|
+
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|temporarily backed off/i.test(
|
|
111
|
+
err,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handle retryable errors with exponential backoff.
|
|
117
|
+
* When multiple credentials are available, marks the failing credential
|
|
118
|
+
* as backed off and retries immediately with the next one.
|
|
119
|
+
* @returns true if retry was initiated, false if max retries exceeded or disabled
|
|
120
|
+
*/
|
|
121
|
+
async handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
122
|
+
const settings = this._deps.settingsManager.getRetrySettings();
|
|
123
|
+
if (!settings.enabled) {
|
|
124
|
+
this._resolveRetry();
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Retry promise is created synchronously in createRetryPromiseForAgentEnd.
|
|
129
|
+
// Keep a defensive fallback here in case a future refactor bypasses that path.
|
|
130
|
+
if (!this._retryPromise) {
|
|
131
|
+
this._retryPromise = new Promise((resolve) => {
|
|
132
|
+
this._retryResolve = resolve;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Try credential fallback before counting against retry budget.
|
|
137
|
+
if (this._deps.getModel() && message.errorMessage) {
|
|
138
|
+
const errorType = this._classifyErrorType(message.errorMessage);
|
|
139
|
+
const isCredentialError = errorType !== "unknown";
|
|
140
|
+
const hasAlternate =
|
|
141
|
+
isCredentialError &&
|
|
142
|
+
this._deps.modelRegistry.authStorage.markUsageLimitReached(
|
|
143
|
+
this._deps.getModel()!.provider,
|
|
144
|
+
this._deps.getSessionId(),
|
|
145
|
+
{ errorType },
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (hasAlternate) {
|
|
149
|
+
this._removeLastAssistantError();
|
|
150
|
+
|
|
151
|
+
this._deps.emit({
|
|
152
|
+
type: "auto_retry_start",
|
|
153
|
+
attempt: this._retryAttempt + 1,
|
|
154
|
+
maxAttempts: settings.maxRetries,
|
|
155
|
+
delayMs: 0,
|
|
156
|
+
errorMessage: `${message.errorMessage} (switching credential)`,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Retry immediately with the next credential - don't increment _retryAttempt
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
this._deps.agent.continue().catch(() => {});
|
|
162
|
+
}, 0);
|
|
163
|
+
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// All credentials are backed off. Try cross-provider fallback before giving up.
|
|
168
|
+
if (isCredentialError) {
|
|
169
|
+
const fallbackResult = await this._deps.fallbackResolver.findFallback(
|
|
170
|
+
this._deps.getModel()!,
|
|
171
|
+
errorType,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (fallbackResult) {
|
|
175
|
+
const previousProvider = this._deps.getModel()!.provider;
|
|
176
|
+
this._deps.agent.setModel(fallbackResult.model);
|
|
177
|
+
this._deps.onModelChange(fallbackResult.model);
|
|
178
|
+
this._removeLastAssistantError();
|
|
179
|
+
|
|
180
|
+
this._deps.emit({
|
|
181
|
+
type: "fallback_provider_switch",
|
|
182
|
+
from: `${previousProvider}/${this._deps.getModel()?.id}`,
|
|
183
|
+
to: `${fallbackResult.model.provider}/${fallbackResult.model.id}`,
|
|
184
|
+
reason: fallbackResult.reason,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this._deps.emit({
|
|
188
|
+
type: "auto_retry_start",
|
|
189
|
+
attempt: this._retryAttempt + 1,
|
|
190
|
+
maxAttempts: settings.maxRetries,
|
|
191
|
+
delayMs: 0,
|
|
192
|
+
errorMessage: `${message.errorMessage} (${fallbackResult.reason})`,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Retry immediately with fallback provider - don't increment _retryAttempt
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
this._deps.agent.continue().catch(() => {});
|
|
198
|
+
}, 0);
|
|
199
|
+
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// No fallback available either
|
|
204
|
+
if (errorType === "quota_exhausted") {
|
|
205
|
+
this._deps.emit({
|
|
206
|
+
type: "fallback_chain_exhausted",
|
|
207
|
+
reason: `All providers exhausted for ${this._deps.getModel()!.provider}/${this._deps.getModel()!.id}`,
|
|
208
|
+
});
|
|
209
|
+
this._deps.emit({
|
|
210
|
+
type: "auto_retry_end",
|
|
211
|
+
success: false,
|
|
212
|
+
attempt: this._retryAttempt,
|
|
213
|
+
finalError: message.errorMessage,
|
|
214
|
+
});
|
|
215
|
+
this._retryAttempt = 0;
|
|
216
|
+
this._resolveRetry();
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this._retryAttempt++;
|
|
223
|
+
|
|
224
|
+
if (this._retryAttempt > settings.maxRetries) {
|
|
225
|
+
this._deps.emit({
|
|
226
|
+
type: "auto_retry_end",
|
|
227
|
+
success: false,
|
|
228
|
+
attempt: this._retryAttempt - 1,
|
|
229
|
+
finalError: message.errorMessage,
|
|
230
|
+
});
|
|
231
|
+
this._retryAttempt = 0;
|
|
232
|
+
this._resolveRetry();
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Use server-requested delay when available, capped by maxDelayMs.
|
|
237
|
+
// Fall back to exponential backoff when no server hint is present.
|
|
238
|
+
const exponentialDelayMs = settings.baseDelayMs * 2 ** (this._retryAttempt - 1);
|
|
239
|
+
let delayMs: number;
|
|
240
|
+
if (message.retryAfterMs !== undefined) {
|
|
241
|
+
const cap = settings.maxDelayMs > 0 ? settings.maxDelayMs : Infinity;
|
|
242
|
+
if (message.retryAfterMs > cap) {
|
|
243
|
+
this._deps.emit({
|
|
244
|
+
type: "auto_retry_end",
|
|
245
|
+
success: false,
|
|
246
|
+
attempt: this._retryAttempt - 1,
|
|
247
|
+
finalError: `Rate limit reset in ${Math.ceil(message.retryAfterMs / 1000)}s (max: ${Math.ceil(cap / 1000)}s). ${message.errorMessage || ""}`.trim(),
|
|
248
|
+
});
|
|
249
|
+
this._retryAttempt = 0;
|
|
250
|
+
this._resolveRetry();
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
delayMs = message.retryAfterMs;
|
|
254
|
+
} else {
|
|
255
|
+
delayMs = exponentialDelayMs;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this._deps.emit({
|
|
259
|
+
type: "auto_retry_start",
|
|
260
|
+
attempt: this._retryAttempt,
|
|
261
|
+
maxAttempts: settings.maxRetries,
|
|
262
|
+
delayMs,
|
|
263
|
+
errorMessage: message.errorMessage || "Unknown error",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this._removeLastAssistantError();
|
|
267
|
+
|
|
268
|
+
// Wait with exponential backoff (abortable)
|
|
269
|
+
this._retryAbortController = new AbortController();
|
|
270
|
+
try {
|
|
271
|
+
await sleep(delayMs, this._retryAbortController.signal);
|
|
272
|
+
} catch {
|
|
273
|
+
// Aborted during sleep
|
|
274
|
+
const attempt = this._retryAttempt;
|
|
275
|
+
this._retryAttempt = 0;
|
|
276
|
+
this._retryAbortController = undefined;
|
|
277
|
+
this._deps.emit({
|
|
278
|
+
type: "auto_retry_end",
|
|
279
|
+
success: false,
|
|
280
|
+
attempt,
|
|
281
|
+
finalError: "Retry cancelled",
|
|
282
|
+
});
|
|
283
|
+
this._resolveRetry();
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
this._retryAbortController = undefined;
|
|
287
|
+
|
|
288
|
+
// Retry via continue() - use setTimeout to break out of event handler chain
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
this._deps.agent.continue().catch(() => {});
|
|
291
|
+
}, 0);
|
|
292
|
+
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Cancel in-progress retry */
|
|
297
|
+
abortRetry(): void {
|
|
298
|
+
this._retryAbortController?.abort();
|
|
299
|
+
this._resolveRetry();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Wait for any in-progress retry to complete.
|
|
304
|
+
* Returns immediately if no retry is in progress.
|
|
305
|
+
*/
|
|
306
|
+
async waitForRetry(): Promise<void> {
|
|
307
|
+
if (this._retryPromise) {
|
|
308
|
+
await this._retryPromise;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Resolve the pending retry promise */
|
|
313
|
+
resolveRetry(): void {
|
|
314
|
+
this._resolveRetry();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// =========================================================================
|
|
318
|
+
// Private helpers
|
|
319
|
+
// =========================================================================
|
|
320
|
+
|
|
321
|
+
private _resolveRetry(): void {
|
|
322
|
+
if (this._retryResolve) {
|
|
323
|
+
this._retryResolve();
|
|
324
|
+
this._retryResolve = undefined;
|
|
325
|
+
this._retryPromise = undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private _findLastAssistantInMessages(
|
|
330
|
+
messages: Array<{ role: string } & Record<string, any>>,
|
|
331
|
+
): AssistantMessage | undefined {
|
|
332
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
333
|
+
const message = messages[i];
|
|
334
|
+
if (message.role === "assistant") {
|
|
335
|
+
return message as AssistantMessage;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Classify an error message into a usage-limit error type for credential backoff.
|
|
343
|
+
*/
|
|
344
|
+
private _classifyErrorType(errorMessage: string): UsageLimitErrorType {
|
|
345
|
+
const err = errorMessage.toLowerCase();
|
|
346
|
+
if (/quota|billing|exceeded.*limit|usage.*limit/i.test(err)) return "quota_exhausted";
|
|
347
|
+
if (/rate.?limit|too many requests|429/i.test(err)) return "rate_limit";
|
|
348
|
+
if (/500|502|503|504|server.?error|internal.?error|service.?unavailable/i.test(err)) return "server_error";
|
|
349
|
+
return "unknown";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Remove the last assistant error message from agent state */
|
|
353
|
+
private _removeLastAssistantError(): void {
|
|
354
|
+
const messages = this._deps.agent.state.messages;
|
|
355
|
+
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
356
|
+
this._deps.agent.replaceMessages(messages.slice(0, -1));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
import { atomicWriteFileSync } from "./fs-utils.js";
|
|
17
17
|
import { readdir, readFile, stat } from "fs/promises";
|
|
18
18
|
import { join, resolve } from "path";
|
|
19
|
-
import lockfile from "proper-lockfile";
|
|
20
19
|
import { getAgentDir as getDefaultAgentDir, getBlobsDir, getSessionsDir } from "../config.js";
|
|
20
|
+
import { tryAcquireLockSync } from "./lock-utils.js";
|
|
21
21
|
import {
|
|
22
22
|
type BashExecutionMessage,
|
|
23
23
|
type CustomMessage,
|
|
@@ -953,39 +953,12 @@ export class SessionManager {
|
|
|
953
953
|
}
|
|
954
954
|
}
|
|
955
955
|
|
|
956
|
-
private acquireSessionLock(path: string): (() => void) | undefined {
|
|
957
|
-
const maxAttempts = 10;
|
|
958
|
-
const delayMs = 20;
|
|
959
|
-
let lastError: unknown;
|
|
960
|
-
|
|
961
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
962
|
-
try {
|
|
963
|
-
return lockfile.lockSync(path, { realpath: false });
|
|
964
|
-
} catch (error) {
|
|
965
|
-
const code =
|
|
966
|
-
typeof error === "object" && error !== null && "code" in error
|
|
967
|
-
? String((error as { code?: unknown }).code)
|
|
968
|
-
: undefined;
|
|
969
|
-
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
|
970
|
-
// Non-fatal: proceed without lock rather than losing data
|
|
971
|
-
return undefined;
|
|
972
|
-
}
|
|
973
|
-
lastError = error;
|
|
974
|
-
const start = Date.now();
|
|
975
|
-
while (Date.now() - start < delayMs) {
|
|
976
|
-
// Busy-wait to avoid async
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
return undefined;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
956
|
private _rewriteFile(): void {
|
|
984
957
|
if (!this.persist || !this.sessionFile) return;
|
|
985
958
|
const content = `${this.fileEntries.map((e) => JSON.stringify(e)).join("\n")}\n`;
|
|
986
959
|
let release: (() => void) | undefined;
|
|
987
960
|
try {
|
|
988
|
-
release =
|
|
961
|
+
release = tryAcquireLockSync(this.sessionFile);
|
|
989
962
|
atomicWriteFileSync(this.sessionFile, content);
|
|
990
963
|
} finally {
|
|
991
964
|
release?.();
|
|
@@ -1024,7 +997,7 @@ export class SessionManager {
|
|
|
1024
997
|
|
|
1025
998
|
let release: (() => void) | undefined;
|
|
1026
999
|
try {
|
|
1027
|
-
release =
|
|
1000
|
+
release = tryAcquireLockSync(this.sessionFile);
|
|
1028
1001
|
if (!this.flushed) {
|
|
1029
1002
|
for (const e of this.fileEntries) {
|
|
1030
1003
|
const prepared = prepareForPersistence(e, this.blobStore) as FileEntry;
|
|
@@ -4,6 +4,7 @@ import { homedir } from "os";
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
5
5
|
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
|
6
6
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
7
|
+
import { toPosixPath } from "../utils/path-display.js";
|
|
7
8
|
import type { ResourceDiagnostic } from "./diagnostics.js";
|
|
8
9
|
|
|
9
10
|
/** Max name length per spec */
|
|
@@ -16,10 +17,6 @@ const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"];
|
|
|
16
17
|
|
|
17
18
|
type IgnoreMatcher = ReturnType<typeof ignore>;
|
|
18
19
|
|
|
19
|
-
function toPosixPath(p: string): string {
|
|
20
|
-
return p.split(sep).join("/");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
function prefixIgnorePattern(line: string, prefix: string): string | null {
|
|
24
21
|
const trimmed = line.trim();
|
|
25
22
|
if (!trimmed) return null;
|
|
@@ -128,14 +128,8 @@ export {
|
|
|
128
128
|
discoverAndLoadExtensions,
|
|
129
129
|
ExtensionRunner,
|
|
130
130
|
importExtensionModule,
|
|
131
|
-
isBashToolResult,
|
|
132
|
-
isEditToolResult,
|
|
133
|
-
isFindToolResult,
|
|
134
|
-
isGrepToolResult,
|
|
135
|
-
isLsToolResult,
|
|
136
|
-
isReadToolResult,
|
|
137
131
|
isToolCallEventType,
|
|
138
|
-
|
|
132
|
+
isToolResultEventType,
|
|
139
133
|
wrapRegisteredTool,
|
|
140
134
|
wrapRegisteredTools,
|
|
141
135
|
wrapToolsWithExtensions,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { unlink } from "node:fs/promises";
|
|
4
|
-
import * as os from "node:os";
|
|
5
4
|
import {
|
|
6
5
|
type Component,
|
|
7
6
|
Container,
|
|
@@ -17,21 +16,19 @@ import {
|
|
|
17
16
|
import { KeybindingsManager } from "../../../core/keybindings.js";
|
|
18
17
|
import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js";
|
|
19
18
|
import { theme } from "../theme/theme.js";
|
|
19
|
+
import { shortenPath } from "../utils/shorten-path.js";
|
|
20
20
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
21
21
|
import { appKey, appKeyHint, keyHint } from "./keybinding-hints.js";
|
|
22
22
|
import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.js";
|
|
23
|
+
import {
|
|
24
|
+
applyRowHighlight,
|
|
25
|
+
buildTreePrefix,
|
|
26
|
+
computeScrollWindow,
|
|
27
|
+
renderCursor,
|
|
28
|
+
} from "./tree-render-utils.js";
|
|
23
29
|
|
|
24
30
|
type SessionScope = "current" | "all";
|
|
25
31
|
|
|
26
|
-
function shortenPath(path: string): string {
|
|
27
|
-
const home = os.homedir();
|
|
28
|
-
if (!path) return path;
|
|
29
|
-
if (path.startsWith(home)) {
|
|
30
|
-
return `~${path.slice(home.length)}`;
|
|
31
|
-
}
|
|
32
|
-
return path;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
function formatSessionDate(date: Date): string {
|
|
36
33
|
const now = new Date();
|
|
37
34
|
const diffMs = now.getTime() - date.getTime();
|
|
@@ -420,11 +417,11 @@ class SessionList implements Component, Focusable {
|
|
|
420
417
|
}
|
|
421
418
|
|
|
422
419
|
// Calculate visible range with scrolling
|
|
423
|
-
const startIndex =
|
|
424
|
-
|
|
425
|
-
|
|
420
|
+
const { startIndex, endIndex } = computeScrollWindow(
|
|
421
|
+
this.selectedIndex,
|
|
422
|
+
this.filteredSessions.length,
|
|
423
|
+
this.maxVisible,
|
|
426
424
|
);
|
|
427
|
-
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);
|
|
428
425
|
|
|
429
426
|
// Render visible sessions (one line each with tree structure)
|
|
430
427
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -435,7 +432,7 @@ class SessionList implements Component, Focusable {
|
|
|
435
432
|
const isCurrent = this.currentSessionFilePath === session.path;
|
|
436
433
|
|
|
437
434
|
// Build tree prefix
|
|
438
|
-
const prefix = this.
|
|
435
|
+
const prefix = this.buildNodeTreePrefix(node);
|
|
439
436
|
|
|
440
437
|
// Session display text (name or first message)
|
|
441
438
|
const hasName = !!session.name;
|
|
@@ -454,7 +451,7 @@ class SessionList implements Component, Focusable {
|
|
|
454
451
|
}
|
|
455
452
|
|
|
456
453
|
// Cursor
|
|
457
|
-
const cursor = isSelected
|
|
454
|
+
const cursor = renderCursor(isSelected);
|
|
458
455
|
|
|
459
456
|
// Calculate available width for message
|
|
460
457
|
const prefixWidth = visibleWidth(prefix);
|
|
@@ -483,11 +480,8 @@ class SessionList implements Component, Focusable {
|
|
|
483
480
|
const spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));
|
|
484
481
|
const styledRight = theme.fg(isConfirmingDelete ? "error" : "dim", rightPart);
|
|
485
482
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
line = theme.bg("selectedBg", line);
|
|
489
|
-
}
|
|
490
|
-
lines.push(truncateToWidth(line, width));
|
|
483
|
+
const line = leftPart + " ".repeat(spacing) + styledRight;
|
|
484
|
+
lines.push(applyRowHighlight(line, isSelected, width));
|
|
491
485
|
}
|
|
492
486
|
|
|
493
487
|
// Add scroll indicator if needed
|
|
@@ -500,14 +494,8 @@ class SessionList implements Component, Focusable {
|
|
|
500
494
|
return lines;
|
|
501
495
|
}
|
|
502
496
|
|
|
503
|
-
private
|
|
504
|
-
|
|
505
|
-
return "";
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const parts = node.ancestorContinues.map((continues) => (continues ? "│ " : " "));
|
|
509
|
-
const branch = node.isLast ? "└─ " : "├─ ";
|
|
510
|
-
return parts.join("") + branch;
|
|
497
|
+
private buildNodeTreePrefix(node: FlatSessionNode): string {
|
|
498
|
+
return buildTreePrefix(node.ancestorContinues, node.isLast, node.depth);
|
|
511
499
|
}
|
|
512
500
|
|
|
513
501
|
handleInput(keyData: string): void {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
1
|
import {
|
|
3
2
|
Box,
|
|
4
3
|
Container,
|
|
@@ -18,6 +17,7 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../../../core/
|
|
|
18
17
|
import { convertToPng } from "../../../utils/image-convert.js";
|
|
19
18
|
import { sanitizeBinaryOutput } from "../../../utils/shell.js";
|
|
20
19
|
import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
|
|
20
|
+
import { shortenPath } from "../utils/shorten-path.js";
|
|
21
21
|
import { renderDiff } from "./diff.js";
|
|
22
22
|
import { keyHint } from "./keybinding-hints.js";
|
|
23
23
|
import { truncateToVisualLines } from "./visual-truncate.js";
|
|
@@ -28,18 +28,6 @@ const BASH_PREVIEW_LINES = 5;
|
|
|
28
28
|
// to keep multiline tokenization mostly correct without re-highlighting the full file.
|
|
29
29
|
const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
* Convert absolute path to tilde notation if it's in home directory
|
|
33
|
-
*/
|
|
34
|
-
function shortenPath(path: unknown): string {
|
|
35
|
-
if (typeof path !== "string") return "";
|
|
36
|
-
const home = os.homedir();
|
|
37
|
-
if (path.startsWith(home)) {
|
|
38
|
-
return `~${path.slice(home.length)}`;
|
|
39
|
-
}
|
|
40
|
-
return path;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
/**
|
|
44
32
|
* Replace tabs with spaces for consistent rendering
|
|
45
33
|
*/
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { truncateToWidth } from "@gsd/pi-tui";
|
|
2
|
+
import { theme } from "../theme/theme.js";
|
|
3
|
+
|
|
4
|
+
// ── Tree connector characters ────────────────────────────────────────
|
|
5
|
+
export const TREE_BRANCH = "\u251C\u2500 "; // "├─ "
|
|
6
|
+
export const TREE_LAST = "\u2514\u2500 "; // "└─ "
|
|
7
|
+
export const TREE_PIPE = "\u2502 "; // "│ "
|
|
8
|
+
export const TREE_SPACE = " "; // 3 spaces
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a tree prefix string from ancestor-continuation flags and branch position.
|
|
12
|
+
*
|
|
13
|
+
* Each ancestor level contributes either a pipe ("│ ") or blank spacing (" ")
|
|
14
|
+
* depending on whether that ancestor has more siblings after it. The final segment
|
|
15
|
+
* is the branch connector: "├─ " (more siblings) or "└─ " (last sibling).
|
|
16
|
+
*
|
|
17
|
+
* Used by session-selector for its simpler flat tree display.
|
|
18
|
+
* tree-selector uses its own gutter-based char-by-char builder for richer rendering.
|
|
19
|
+
*/
|
|
20
|
+
export function buildTreePrefix(ancestorContinues: boolean[], isLast: boolean, depth: number): string {
|
|
21
|
+
if (depth === 0) return "";
|
|
22
|
+
const parts = ancestorContinues.map((continues) => (continues ? TREE_PIPE : TREE_SPACE));
|
|
23
|
+
const branch = isLast ? TREE_LAST : TREE_BRANCH;
|
|
24
|
+
return parts.join("") + branch;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Scroll window ────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export interface ScrollWindow {
|
|
30
|
+
/** First visible index (inclusive) */
|
|
31
|
+
startIndex: number;
|
|
32
|
+
/** Last visible index (exclusive) */
|
|
33
|
+
endIndex: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute a centered scroll window around `selectedIndex` within a list of `totalItems`.
|
|
38
|
+
*
|
|
39
|
+
* The window tries to center the selected item. When near the beginning or end of the
|
|
40
|
+
* list the window clamps so it doesn't exceed bounds.
|
|
41
|
+
*/
|
|
42
|
+
export function computeScrollWindow(selectedIndex: number, totalItems: number, maxVisible: number): ScrollWindow {
|
|
43
|
+
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), totalItems - maxVisible));
|
|
44
|
+
const endIndex = Math.min(startIndex + maxVisible, totalItems);
|
|
45
|
+
return { startIndex, endIndex };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Cursor & selection helpers ───────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Return the cursor indicator for a list row.
|
|
52
|
+
*
|
|
53
|
+
* Selected: "› " (accent-colored)
|
|
54
|
+
* Unselected: " " (two spaces, matching width)
|
|
55
|
+
*/
|
|
56
|
+
export function renderCursor(isSelected: boolean): string {
|
|
57
|
+
return isSelected ? theme.fg("accent", "\u203A ") : " ";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Apply selected-row background highlight and truncate to `width`.
|
|
62
|
+
*/
|
|
63
|
+
export function applyRowHighlight(line: string, isSelected: boolean, width: number): string {
|
|
64
|
+
const truncated = truncateToWidth(line, width);
|
|
65
|
+
return isSelected ? theme.bg("selectedBg", truncated) : truncated;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Scroll position indicator ────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render a muted "(current/total)" position indicator, optionally with a suffix label.
|
|
72
|
+
*/
|
|
73
|
+
export function renderScrollPosition(
|
|
74
|
+
selectedIndex: number,
|
|
75
|
+
totalItems: number,
|
|
76
|
+
width: number,
|
|
77
|
+
suffixLabel?: string,
|
|
78
|
+
): string {
|
|
79
|
+
const suffix = suffixLabel ?? "";
|
|
80
|
+
return truncateToWidth(theme.fg("muted", ` (${selectedIndex + 1}/${totalItems})${suffix}`), width);
|
|
81
|
+
}
|