@gajae-code/coding-agent 0.15.6 → 0.16.0
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 +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
|
@@ -9,6 +9,7 @@ import * as git from "../../utils/git";
|
|
|
9
9
|
import { sanitizeStatusText } from "../shared";
|
|
10
10
|
import { getContextUsageLevel, getContextUsageThemeColor } from "./status-line/context-thresholds";
|
|
11
11
|
import { resolveCurrentBranch } from "./status-line/git-utils";
|
|
12
|
+
import { shortenModelId } from "./status-line/model-name";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Footer component that shows pwd, token stats, and context usage
|
|
@@ -191,8 +192,9 @@ export class FooterComponent implements Component {
|
|
|
191
192
|
|
|
192
193
|
let statsLeft = statsParts.join(" ");
|
|
193
194
|
|
|
194
|
-
// Add model name on the right side, plus thinking level if model supports it
|
|
195
|
-
|
|
195
|
+
// Add model name on the right side, plus thinking level if model supports it.
|
|
196
|
+
// Same shortest-recognizable form the status rail renders.
|
|
197
|
+
const modelName = shortenModelId(state.model?.id ?? state.model?.name);
|
|
196
198
|
|
|
197
199
|
// Add thinking level hint when the current model advertises supported efforts
|
|
198
200
|
let rightSide = modelName;
|
|
@@ -1,9 +1,55 @@
|
|
|
1
|
-
import { Container, matchesKey, Spacer, TruncatedText } from "@gajae-code/tui";
|
|
1
|
+
import { Container, matchesKey, type SelectItem, SelectList, Spacer, TruncatedText } from "@gajae-code/tui";
|
|
2
2
|
import type { OnboardingProfile } from "../../setup/frictionless-onboarding";
|
|
3
|
-
import { theme } from "../theme/theme";
|
|
3
|
+
import { getSelectListTheme, theme } from "../theme/theme";
|
|
4
|
+
import { UI_LANGUAGES, type UiLanguage } from "../ui-language";
|
|
4
5
|
import { matchesSelectCancel } from "../utils/keybinding-matchers";
|
|
5
6
|
import { DynamicBorder } from "./dynamic-border";
|
|
6
7
|
|
|
8
|
+
export const UI_LANGUAGE_CHOICES: readonly { value: UiLanguage; label: string }[] = [
|
|
9
|
+
{ value: "en", label: "English" },
|
|
10
|
+
{ value: "ko", label: "한국어" },
|
|
11
|
+
{ value: "zh", label: "简体中文" },
|
|
12
|
+
{ value: "ja", label: "日本語" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export class InterfaceLanguageSelectorComponent extends Container {
|
|
16
|
+
readonly #selectList: SelectList;
|
|
17
|
+
|
|
18
|
+
constructor(onSelect: (language: UiLanguage) => void, onCancel: () => void) {
|
|
19
|
+
super();
|
|
20
|
+
this.addChild(new DynamicBorder());
|
|
21
|
+
this.addChild(new Spacer(1));
|
|
22
|
+
this.addChild(
|
|
23
|
+
new TruncatedText(
|
|
24
|
+
theme.bold(
|
|
25
|
+
"Choose your interface language / 언어를 선택하세요 / 选择界面语言 / インターフェース言語を選択",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
this.addChild(new Spacer(1));
|
|
30
|
+
const items: SelectItem[] = UI_LANGUAGE_CHOICES.map(choice => ({ value: choice.value, label: choice.label }));
|
|
31
|
+
this.#selectList = new SelectList(items, items.length, getSelectListTheme());
|
|
32
|
+
this.#selectList.onSelect = item => {
|
|
33
|
+
if (UI_LANGUAGES.includes(item.value as UiLanguage)) onSelect(item.value as UiLanguage);
|
|
34
|
+
};
|
|
35
|
+
this.#selectList.onCancel = onCancel;
|
|
36
|
+
this.addChild(this.#selectList);
|
|
37
|
+
this.addChild(new Spacer(1));
|
|
38
|
+
this.addChild(new TruncatedText(theme.fg("dim", "↑/↓ select · Enter choose · Esc use English")));
|
|
39
|
+
this.addChild(new DynamicBorder());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
handleInput(keyData: string): void {
|
|
43
|
+
// The app.exit action is owned by the parent editor and is not a TUI
|
|
44
|
+
// selector key id. Ctrl-D is its default byte when the editor is empty.
|
|
45
|
+
if (keyData === "\x04") {
|
|
46
|
+
this.#selectList.onCancel?.();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.#selectList.handleInput(keyData);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
7
53
|
export type FrictionlessOnboardingAction =
|
|
8
54
|
| "analyze"
|
|
9
55
|
| "apply"
|
|
@@ -56,6 +56,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
56
56
|
#onCancelCallback: () => void;
|
|
57
57
|
#statusMessage: string | undefined;
|
|
58
58
|
#validateAuthCallback?: (providerId: string) => Promise<boolean>;
|
|
59
|
+
#onValidationError?: (error: unknown) => boolean;
|
|
59
60
|
#requestRenderCallback?: () => void;
|
|
60
61
|
#authState: Map<string, "checking" | "valid" | "invalid"> = new Map();
|
|
61
62
|
#externalCredentialCandidates: ImportableCredential[] = [];
|
|
@@ -76,6 +77,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
76
77
|
onCancel: () => void,
|
|
77
78
|
options?: {
|
|
78
79
|
validateAuth?: (providerId: string) => Promise<boolean>;
|
|
80
|
+
onValidationError?: (error: unknown) => boolean;
|
|
79
81
|
requestRender?: () => void;
|
|
80
82
|
externalCredentialCandidates?: ImportableCredential[];
|
|
81
83
|
} & Partial<OAuthSelectorAccountOptions>,
|
|
@@ -86,6 +88,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
86
88
|
this.#onSelectCallback = onSelect;
|
|
87
89
|
this.#onCancelCallback = onCancel;
|
|
88
90
|
this.#validateAuthCallback = options?.validateAuth;
|
|
91
|
+
this.#onValidationError = options?.onValidationError;
|
|
89
92
|
this.#requestRenderCallback = options?.requestRender;
|
|
90
93
|
this.#externalCredentialCandidates = options?.externalCredentialCandidates ?? [];
|
|
91
94
|
this.#accountProviderId = options?.accountProviderId;
|
|
@@ -194,7 +197,9 @@ export class OAuthSelectorComponent extends Container {
|
|
|
194
197
|
let isValid = false;
|
|
195
198
|
try {
|
|
196
199
|
isValid = await this.#validateAuthCallback(providerId);
|
|
197
|
-
} catch {
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (generation !== this.#validationGeneration) return;
|
|
202
|
+
if (this.#onValidationError?.(error)) return;
|
|
198
203
|
isValid = false;
|
|
199
204
|
}
|
|
200
205
|
if (generation !== this.#validationGeneration) return;
|
|
@@ -1009,6 +1009,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
1009
1009
|
options = [
|
|
1010
1010
|
{ value: "en", label: uiString(language, "settings.language.english") },
|
|
1011
1011
|
{ value: "ko", label: uiString(language, "settings.language.korean") },
|
|
1012
|
+
{ value: "zh", label: uiString(language, "settings.language.chinese") },
|
|
1012
1013
|
{ value: "ja", label: uiString(language, "settings.language.japanese") },
|
|
1013
1014
|
];
|
|
1014
1015
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortest recognizable model label, derived purely from the model id.
|
|
3
|
+
*
|
|
4
|
+
* The rail and the footer both render this form, so a narrow terminal spends
|
|
5
|
+
* its cells on the part of the id that actually identifies the model:
|
|
6
|
+
*
|
|
7
|
+
* `anthropic/claude-sonnet-4-5-20250929` -> `sonnet-4.5`
|
|
8
|
+
* `openai/gpt-4o-2024-05-13` -> `gpt-4o`
|
|
9
|
+
* `google/gemini-3-pro` -> `gemini-3-pro`
|
|
10
|
+
* `qwen2.5:7b` -> `qwen2.5:7b`
|
|
11
|
+
*
|
|
12
|
+
* Deliberately a pure heuristic with no curated override table: every provider
|
|
13
|
+
* in `models.json` must degrade sanely without per-model maintenance. The
|
|
14
|
+
* function never returns an empty string — an id that the heuristic would
|
|
15
|
+
* strip to nothing falls back to the previous, longer form.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Vendor family words that carry no information once the provider prefix is
|
|
20
|
+
* gone. Deliberately minimal: `gpt` and `gemini` are part of how those models
|
|
21
|
+
* are named and are kept.
|
|
22
|
+
*/
|
|
23
|
+
const VENDOR_FAMILY_WORDS: ReadonlySet<string> = new Set(["claude"]);
|
|
24
|
+
|
|
25
|
+
/** Trailing snapshot dates and build stamps: `-20250929`, `-2024-05-13`, `-v2`. */
|
|
26
|
+
const DATE_OR_BUILD_SUFFIX = /-(?:\d{4}-\d{2}-\d{2}|\d{8}|\d{6}|v\d+(?:\.\d+)*)$/;
|
|
27
|
+
|
|
28
|
+
/** Split version parts read as one number: `sonnet-4-5` -> `sonnet-4.5`. */
|
|
29
|
+
const SPLIT_VERSION = /(\d)-(\d)/g;
|
|
30
|
+
|
|
31
|
+
export const NO_MODEL_LABEL = "no-model";
|
|
32
|
+
|
|
33
|
+
export function shortenModelId(rawId: string | undefined | null): string {
|
|
34
|
+
const raw = (rawId ?? "").trim();
|
|
35
|
+
if (!raw) return NO_MODEL_LABEL;
|
|
36
|
+
|
|
37
|
+
// Provider prefix: `anthropic/claude-…`, `openrouter/anthropic/claude-…`.
|
|
38
|
+
const afterProvider = raw.slice(raw.lastIndexOf("/") + 1).trim() || raw;
|
|
39
|
+
|
|
40
|
+
const withoutDate = afterProvider.replace(DATE_OR_BUILD_SUFFIX, "") || afterProvider;
|
|
41
|
+
|
|
42
|
+
let name = withoutDate;
|
|
43
|
+
const dashIndex = name.indexOf("-");
|
|
44
|
+
if (dashIndex > 0) {
|
|
45
|
+
const head = name.slice(0, dashIndex).toLowerCase();
|
|
46
|
+
const rest = name.slice(dashIndex + 1);
|
|
47
|
+
if (rest && VENDOR_FAMILY_WORDS.has(head)) name = rest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
name = name.replace(SPLIT_VERSION, "$1.$2");
|
|
51
|
+
|
|
52
|
+
return name || withoutDate || afterProvider;
|
|
53
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow-width survival row for the status rail.
|
|
3
|
+
*
|
|
4
|
+
* The rail evicts segments continuously as the terminal narrows. Without this
|
|
5
|
+
* row the last thing left at very small widths is the `…+N` overflow marker —
|
|
6
|
+
* the three things a user actually needs (how much context is left, whether a
|
|
7
|
+
* goal is running, which model is answering) are exactly what the generic
|
|
8
|
+
* eviction throws away first or last by accident.
|
|
9
|
+
*
|
|
10
|
+
* This row keeps them, in priority order, by stepping each item down its own
|
|
11
|
+
* ladder instead of dropping it:
|
|
12
|
+
*
|
|
13
|
+
* full form -> shortened form -> single glyph -> dropped
|
|
14
|
+
*
|
|
15
|
+
* Context % is never dropped and is the last survivor; the model name is
|
|
16
|
+
* dropped first of the three. The row is rendered as bare colored text with no
|
|
17
|
+
* group padding or end caps, so — like the overflow marker — it can survive at
|
|
18
|
+
* widths where a normal segment group cannot.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { visibleWidth } from "@gajae-code/tui";
|
|
22
|
+
import { formatNumber } from "@gajae-code/utils";
|
|
23
|
+
import { theme } from "../../theme/theme";
|
|
24
|
+
import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
|
|
25
|
+
import { shortenModelId } from "./model-name";
|
|
26
|
+
import { goalStatusDisplay } from "./segments";
|
|
27
|
+
import type { SegmentContext } from "./types";
|
|
28
|
+
|
|
29
|
+
const SEPARATOR = "·";
|
|
30
|
+
|
|
31
|
+
/** Which priority items the active layout is supposed to be carrying. */
|
|
32
|
+
export interface PriorityItemSet {
|
|
33
|
+
context: boolean;
|
|
34
|
+
goal: boolean;
|
|
35
|
+
model: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function contextForms(ctx: SegmentContext): { full: string; short: string } | null {
|
|
39
|
+
const window = ctx.contextWindow;
|
|
40
|
+
if (!(window > 0)) return null;
|
|
41
|
+
|
|
42
|
+
const raw = ctx.contextPercent;
|
|
43
|
+
const pct = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
|
|
44
|
+
const color = pct === null ? "statusLineContext" : getContextUsageThemeColor(getContextUsageLevel(pct, window));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
full: theme.fg(color, `${pct === null ? "?" : `${pct.toFixed(1)}%`}/${formatNumber(window)}`),
|
|
48
|
+
short: theme.fg(color, pct === null ? "?" : `${Math.round(pct)}%`),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function goalForms(ctx: SegmentContext): { label: string; glyph: string } | null {
|
|
53
|
+
const display = goalStatusDisplay(ctx);
|
|
54
|
+
if (!display) return null;
|
|
55
|
+
|
|
56
|
+
const glyph = display.icon || "G";
|
|
57
|
+
return {
|
|
58
|
+
label: theme.fg(display.color, display.icon ? `${display.icon} Goal` : "Goal"),
|
|
59
|
+
glyph: theme.fg(display.color, glyph),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function modelForms(ctx: SegmentContext): { short: string; glyph: string } {
|
|
64
|
+
const model = ctx.session.state.model;
|
|
65
|
+
const name = shortenModelId(model?.id ?? model?.name);
|
|
66
|
+
return {
|
|
67
|
+
short: theme.fg("statusLineModel", name),
|
|
68
|
+
glyph: theme.fg("statusLineModel", theme.icon.model || name.slice(0, 1)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Widest priority row that fits `width`, or null when even the minimal form
|
|
74
|
+
* cannot fit (callers then fall back to the overflow marker).
|
|
75
|
+
*
|
|
76
|
+
* Returns null when neither context nor goal is available: the model name alone
|
|
77
|
+
* is the lowest-priority item and never justifies replacing the normal rail.
|
|
78
|
+
*/
|
|
79
|
+
export function buildPriorityRow(ctx: SegmentContext, width: number, include: PriorityItemSet): string | null {
|
|
80
|
+
if (width <= 0) return null;
|
|
81
|
+
|
|
82
|
+
const context = include.context ? contextForms(ctx) : null;
|
|
83
|
+
const goal = include.goal ? goalForms(ctx) : null;
|
|
84
|
+
const model = include.model ? modelForms(ctx) : null;
|
|
85
|
+
if (!context && !goal) return null;
|
|
86
|
+
|
|
87
|
+
// Ordered widest-first. Every item degrades through its own forms before any
|
|
88
|
+
// item is dropped, and drops run lowest-priority first (model, then goal).
|
|
89
|
+
const ladder: readonly (readonly (string | null)[])[] = [
|
|
90
|
+
[context?.full ?? null, goal?.label ?? null, model?.short ?? null],
|
|
91
|
+
[context?.full ?? null, goal?.glyph ?? null, model?.short ?? null],
|
|
92
|
+
[context?.full ?? null, goal?.glyph ?? null, model?.glyph ?? null],
|
|
93
|
+
[context?.short ?? null, goal?.glyph ?? null, model?.glyph ?? null],
|
|
94
|
+
[context?.short ?? null, goal?.glyph ?? null, null],
|
|
95
|
+
[context?.short ?? null, null, null],
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const separator = theme.fg("dim", SEPARATOR);
|
|
99
|
+
for (const candidate of ladder) {
|
|
100
|
+
const parts = candidate.filter((part): part is string => part !== null);
|
|
101
|
+
if (parts.length === 0) continue;
|
|
102
|
+
const row = parts.join(separator);
|
|
103
|
+
if (visibleWidth(row) <= width) return row;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -8,6 +8,7 @@ import { shortenPath } from "../../../tools/render-utils";
|
|
|
8
8
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
|
|
9
9
|
import { sanitizeStatusText } from "../../shared";
|
|
10
10
|
import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
|
|
11
|
+
import { shortenModelId } from "./model-name";
|
|
11
12
|
import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
|
|
12
13
|
|
|
13
14
|
export type { SegmentContext } from "./types";
|
|
@@ -99,10 +100,10 @@ const modelSegment: StatusLineSegment = {
|
|
|
99
100
|
const state = ctx.session.state;
|
|
100
101
|
const opts = ctx.options.model ?? {};
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
// Shortest recognizable id-derived form (`sonnet-4.5`), the same label the
|
|
104
|
+
// footer renders, so a narrow rail never spends cells on a provider prefix
|
|
105
|
+
// or a snapshot date.
|
|
106
|
+
const modelName = shortenModelId(state.model?.id ?? state.model?.name);
|
|
106
107
|
|
|
107
108
|
let content = withIcon(theme.icon.model, modelName);
|
|
108
109
|
|
|
@@ -148,9 +149,16 @@ function formatGoalUsage(current: number): string {
|
|
|
148
149
|
return used;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Icon and color for the active goal indicator, or null when goal mode is not
|
|
154
|
+
* showing. Shared with the narrow-width priority row so the compact glyph keeps
|
|
155
|
+
* the same status color as the full label.
|
|
156
|
+
*/
|
|
157
|
+
export function goalStatusDisplay(ctx: SegmentContext): { icon: string; color: ThemeColor } | null {
|
|
158
|
+
const mode = ctx.goalMode;
|
|
159
|
+
if (!mode || (!mode.enabled && !mode.paused)) return null;
|
|
160
|
+
|
|
161
|
+
const status = ctx.session.getGoalModeState?.()?.goal?.status ?? (mode.paused ? "paused" : "active");
|
|
154
162
|
|
|
155
163
|
let icon: string = theme.icon.goal;
|
|
156
164
|
let color: ThemeColor = "accent";
|
|
@@ -170,13 +178,19 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
|
|
|
170
178
|
default:
|
|
171
179
|
break;
|
|
172
180
|
}
|
|
181
|
+
return { icon, color };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: boolean }): RenderedSegment {
|
|
185
|
+
const display = goalStatusDisplay(ctx) ?? { icon: theme.icon.goal, color: "accent" as ThemeColor };
|
|
186
|
+
const goal = ctx.session.getGoalModeState?.()?.goal;
|
|
173
187
|
|
|
174
|
-
const parts: string[] = [withIcon(icon, "Goal")];
|
|
188
|
+
const parts: string[] = [withIcon(display.icon, "Goal")];
|
|
175
189
|
const showUsage = ctx.session.settings.get("goal.statusInFooter") === true;
|
|
176
190
|
if (showUsage && goal) {
|
|
177
191
|
parts.push(formatGoalUsage(goal.tokensUsed));
|
|
178
192
|
}
|
|
179
|
-
return { content: theme.fg(color, parts.join(" ")), visible:
|
|
193
|
+
return { content: theme.fg(display.color, parts.join(" ")), visible: mode.enabled || mode.paused };
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
const modeSegment: StatusLineSegment = {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveCurrentBranch,
|
|
29
29
|
} from "./status-line/git-utils";
|
|
30
30
|
import { getPreset } from "./status-line/presets";
|
|
31
|
+
import { buildPriorityRow, type PriorityItemSet } from "./status-line/priority-row";
|
|
31
32
|
import { renderSegment, type SegmentContext } from "./status-line/segments";
|
|
32
33
|
import { getSeparator } from "./status-line/separators";
|
|
33
34
|
import { calculateTokensPerSecond } from "./status-line/token-rate";
|
|
@@ -132,6 +133,8 @@ interface CollectedStatusSegments {
|
|
|
132
133
|
left: string[];
|
|
133
134
|
leftSegIds: StatusLineSegmentId[];
|
|
134
135
|
right: string[];
|
|
136
|
+
/** Parallel to `right`; null for action hints, job counts and the version tag. */
|
|
137
|
+
rightSegIds: (StatusLineSegmentId | null)[];
|
|
135
138
|
previewHighlightSegment: StatusLineSegmentId | undefined;
|
|
136
139
|
sessionAccent: boolean | undefined;
|
|
137
140
|
leftSepWidth: number;
|
|
@@ -819,6 +822,7 @@ export class StatusLineComponent implements Component {
|
|
|
819
822
|
}
|
|
820
823
|
|
|
821
824
|
const right: string[] = [];
|
|
825
|
+
const rightSegIds: (StatusLineSegmentId | null)[] = [];
|
|
822
826
|
const actionHints =
|
|
823
827
|
effectiveSettings.showActionHints === false
|
|
824
828
|
? []
|
|
@@ -833,9 +837,13 @@ export class StatusLineComponent implements Component {
|
|
|
833
837
|
const rendered = renderSegment(segId, ctx);
|
|
834
838
|
if (rendered.visible && rendered.content) {
|
|
835
839
|
right.push(highlightSegment(segId, rendered.content));
|
|
840
|
+
rightSegIds.push(segId);
|
|
836
841
|
}
|
|
837
842
|
}
|
|
838
|
-
|
|
843
|
+
for (const hint of actionHints) {
|
|
844
|
+
right.push(hint.content);
|
|
845
|
+
rightSegIds.push(null);
|
|
846
|
+
}
|
|
839
847
|
|
|
840
848
|
const runningBackgroundJobs =
|
|
841
849
|
this.session.getAsyncJobSnapshot()?.running.filter(job => job.metadata?.monitor !== true).length ?? 0;
|
|
@@ -843,9 +851,11 @@ export class StatusLineComponent implements Component {
|
|
|
843
851
|
const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
|
|
844
852
|
const label = `${formatCount("job", runningBackgroundJobs)} running`;
|
|
845
853
|
right.push(theme.fg("statusLineSubagents", `${icon}${label}`));
|
|
854
|
+
rightSegIds.push(null);
|
|
846
855
|
}
|
|
847
856
|
if (this.#version) {
|
|
848
857
|
right.push(theme.fg("dim", `v${this.#version}`));
|
|
858
|
+
rightSegIds.push(null);
|
|
849
859
|
}
|
|
850
860
|
|
|
851
861
|
return {
|
|
@@ -857,6 +867,7 @@ export class StatusLineComponent implements Component {
|
|
|
857
867
|
left,
|
|
858
868
|
leftSegIds,
|
|
859
869
|
right,
|
|
870
|
+
rightSegIds,
|
|
860
871
|
previewHighlightSegment,
|
|
861
872
|
sessionAccent: effectiveSettings.sessionAccent,
|
|
862
873
|
leftSepWidth: visibleWidth(separatorDef.left),
|
|
@@ -872,22 +883,108 @@ export class StatusLineComponent implements Component {
|
|
|
872
883
|
return Math.max(1, Math.min(3, Math.trunc(raw)));
|
|
873
884
|
}
|
|
874
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Which priority items this layout is carrying, and where.
|
|
888
|
+
*
|
|
889
|
+
* Context % has no segment of its own in most presets — it rides inside the
|
|
890
|
+
* `model` segment — so `model` inherits the context rank whenever it does.
|
|
891
|
+
* Items whose data is absent (no context window, no active goal) carry no
|
|
892
|
+
* priority at all, which keeps a rail with nothing to protect evicting
|
|
893
|
+
* exactly as it always did.
|
|
894
|
+
*/
|
|
895
|
+
#priorityItems(seg: CollectedStatusSegments): {
|
|
896
|
+
include: PriorityItemSet;
|
|
897
|
+
inlineContextPct: boolean;
|
|
898
|
+
} {
|
|
899
|
+
const collected = (id: StatusLineSegmentId): boolean =>
|
|
900
|
+
seg.leftSegIds.includes(id) || seg.rightSegIds.includes(id);
|
|
901
|
+
const inlineContextPct =
|
|
902
|
+
seg.ctx.options.model?.showContextPercent !== false && seg.ctx.contextPctSegmentActive !== true;
|
|
903
|
+
const goalMode = seg.ctx.goalMode;
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
inlineContextPct,
|
|
907
|
+
include: {
|
|
908
|
+
context:
|
|
909
|
+
seg.ctx.contextWindow > 0 && (collected("context_pct") || (collected("model") && inlineContextPct)),
|
|
910
|
+
goal: collected("mode") && goalMode !== null && (goalMode.enabled || goalMode.paused),
|
|
911
|
+
model: collected("model"),
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Eviction rank: 0 is ordinary telemetry and goes first, 3 is context % and
|
|
918
|
+
* goes last. The model name outranks telemetry but loses to the goal
|
|
919
|
+
* indicator, matching context % > goal > model.
|
|
920
|
+
*/
|
|
921
|
+
#priorityRanker(seg: CollectedStatusSegments): (id: StatusLineSegmentId | null) => number {
|
|
922
|
+
const { include, inlineContextPct } = this.#priorityItems(seg);
|
|
923
|
+
const anyPriority = include.context || include.goal;
|
|
924
|
+
|
|
925
|
+
return (id: StatusLineSegmentId | null): number => {
|
|
926
|
+
if (id === null || !anyPriority) return 0;
|
|
927
|
+
if (id === "context_pct") return include.context ? 3 : 0;
|
|
928
|
+
if (id === "model") {
|
|
929
|
+
if (include.context && inlineContextPct) return 3;
|
|
930
|
+
return include.model ? 1 : 0;
|
|
931
|
+
}
|
|
932
|
+
if (id === "mode") return include.goal ? 2 : 0;
|
|
933
|
+
return 0;
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Priority row for a layout that could not keep everything the user needs on
|
|
939
|
+
* screen, or null when the normal rail already carries context %, the goal
|
|
940
|
+
* indicator and the model name.
|
|
941
|
+
*
|
|
942
|
+
* Context % has no segment of its own in most presets — it rides inside the
|
|
943
|
+
* `model` segment — so losing `model` silently loses the highest-priority
|
|
944
|
+
* item too. Survival is therefore resolved per item, not per segment.
|
|
945
|
+
*/
|
|
946
|
+
#priorityRowFor(
|
|
947
|
+
seg: CollectedStatusSegments,
|
|
948
|
+
survivingLeftIds: readonly (StatusLineSegmentId | null)[],
|
|
949
|
+
survivingRightIds: readonly (StatusLineSegmentId | null)[],
|
|
950
|
+
width: number,
|
|
951
|
+
): string | null {
|
|
952
|
+
const survived = (id: StatusLineSegmentId): boolean =>
|
|
953
|
+
survivingLeftIds.includes(id) || survivingRightIds.includes(id);
|
|
954
|
+
|
|
955
|
+
const { include, inlineContextPct } = this.#priorityItems(seg);
|
|
956
|
+
if (!include.context && !include.goal) return null;
|
|
957
|
+
|
|
958
|
+
const contextLost = include.context && !survived("context_pct") && !(inlineContextPct && survived("model"));
|
|
959
|
+
const goalLost = include.goal && !survived("mode");
|
|
960
|
+
const modelLost = include.model && !survived("model");
|
|
961
|
+
if (!contextLost && !goalLost && !modelLost) return null;
|
|
962
|
+
|
|
963
|
+
return buildPriorityRow(seg.ctx, width, include);
|
|
964
|
+
}
|
|
965
|
+
|
|
875
966
|
#buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
|
|
876
967
|
const seg = precollected ?? this.#collectStatusSegments(width, this.#resolveSettings());
|
|
877
968
|
const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
|
|
878
969
|
const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
|
|
879
970
|
const topFillWidth = Math.max(0, width);
|
|
880
971
|
|
|
972
|
+
const rank = this.#priorityRanker(seg);
|
|
973
|
+
|
|
881
974
|
/**
|
|
882
|
-
* Evict against `budget
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
975
|
+
* Evict against `budget`: shrink the path first, then drop the
|
|
976
|
+
* lowest-priority segment still standing — right side before left, tail
|
|
977
|
+
* first within a side, which is the historical order for everything that
|
|
978
|
+
* carries no priority. Context %, the goal indicator and the model name
|
|
979
|
+
* outrank ordinary telemetry and are therefore the last to go. Runs from
|
|
980
|
+
* the original segment lists each time so the marker reservation can be
|
|
981
|
+
* recomputed without compounding evictions.
|
|
886
982
|
*/
|
|
887
983
|
const layout = (budget: number) => {
|
|
888
984
|
const left = [...seg.left];
|
|
889
985
|
const leftIds = [...seg.leftSegIds];
|
|
890
986
|
const right = [...seg.right];
|
|
987
|
+
const rightIds = [...seg.rightSegIds];
|
|
891
988
|
let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
892
989
|
let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
893
990
|
const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
|
|
@@ -905,19 +1002,42 @@ export class StatusLineComponent implements Component {
|
|
|
905
1002
|
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
906
1003
|
}
|
|
907
1004
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1005
|
+
// Lowest rank loses first; equal ranks fall back to the historical
|
|
1006
|
+
// right-then-left, tail-first order.
|
|
1007
|
+
while (totalWidth() > budget && (left.length > 0 || right.length > 0)) {
|
|
1008
|
+
let victimSide: "left" | "right" | null = null;
|
|
1009
|
+
let victimIndex = -1;
|
|
1010
|
+
let victimRank = Number.POSITIVE_INFINITY;
|
|
1011
|
+
for (let index = right.length - 1; index >= 0; index -= 1) {
|
|
1012
|
+
const candidate = rank(rightIds[index]);
|
|
1013
|
+
if (candidate < victimRank) {
|
|
1014
|
+
victimRank = candidate;
|
|
1015
|
+
victimSide = "right";
|
|
1016
|
+
victimIndex = index;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
for (let index = left.length - 1; index >= 0; index -= 1) {
|
|
1020
|
+
const candidate = rank(leftIds[index]);
|
|
1021
|
+
if (candidate < victimRank) {
|
|
1022
|
+
victimRank = candidate;
|
|
1023
|
+
victimSide = "left";
|
|
1024
|
+
victimIndex = index;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (victimSide === null) break;
|
|
1028
|
+
if (victimSide === "right") {
|
|
1029
|
+
right.splice(victimIndex, 1);
|
|
1030
|
+
rightIds.splice(victimIndex, 1);
|
|
1031
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
1032
|
+
} else {
|
|
1033
|
+
left.splice(victimIndex, 1);
|
|
1034
|
+
leftIds.splice(victimIndex, 1);
|
|
1035
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
1036
|
+
}
|
|
916
1037
|
dropped += 1;
|
|
917
|
-
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
918
1038
|
}
|
|
919
1039
|
}
|
|
920
|
-
return { left, right, leftWidth, rightWidth, dropped };
|
|
1040
|
+
return { left, right, leftIds, rightIds, leftWidth, rightWidth, dropped };
|
|
921
1041
|
};
|
|
922
1042
|
|
|
923
1043
|
// First pass with the full budget establishes whether anything is lost at
|
|
@@ -939,6 +1059,11 @@ export class StatusLineComponent implements Component {
|
|
|
939
1059
|
}
|
|
940
1060
|
}
|
|
941
1061
|
const marker = this.#renderOverflowMarker(placed.dropped, reserved);
|
|
1062
|
+
// Nothing survived that the user actually needs? Spend the rail on the
|
|
1063
|
+
// priority row instead of an overflow marker. Suppressing the marker here
|
|
1064
|
+
// is deliberate: at these widths its cells are worth more as context %.
|
|
1065
|
+
const priorityRow = this.#priorityRowFor(seg, placed.leftIds, placed.rightIds, topFillWidth);
|
|
1066
|
+
if (priorityRow !== null) return priorityRow;
|
|
942
1067
|
|
|
943
1068
|
const leftGroup = this.#renderStatusGroup(placed.left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
944
1069
|
const rightGroup = this.#renderStatusGroup(placed.right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
@@ -1078,6 +1203,17 @@ export class StatusLineComponent implements Component {
|
|
|
1078
1203
|
if (row.length === 0) packedRows.splice(index, 1);
|
|
1079
1204
|
}
|
|
1080
1205
|
|
|
1206
|
+
// Wrapping is supposed to be the lossless path. When even `maxRows` rows
|
|
1207
|
+
// cannot hold everything, the priority row beats a partial rail plus a
|
|
1208
|
+
// count of what is missing.
|
|
1209
|
+
if (dropped > 0) {
|
|
1210
|
+
const priorityRow = this.#priorityRowFor(seg, [], [], topFillWidth);
|
|
1211
|
+
if (priorityRow !== null) {
|
|
1212
|
+
this.#renderedRowsCache = { key: cacheKey, rows: [priorityRow] };
|
|
1213
|
+
return [priorityRow];
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1081
1217
|
// Reserve marker cells on whichever row ends up last, evicting from its
|
|
1082
1218
|
// tail until the cue fits. Never opens a new row: that would contradict
|
|
1083
1219
|
// maxRows. Emptying that row is fine -- the marker then occupies it alone.
|
|
@@ -420,6 +420,34 @@ export class InputController {
|
|
|
420
420
|
if (this.ctx.cancelPendingSubmission()) {
|
|
421
421
|
return true;
|
|
422
422
|
}
|
|
423
|
+
if (options.processes && this.ctx.session.isBashRunning) {
|
|
424
|
+
this.ctx.session.abortBash();
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (options.processes && this.ctx.session.isEvalRunning) {
|
|
428
|
+
this.ctx.session.abortEval();
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
// A queued steer on a live stream is owned by the steering branch below,
|
|
432
|
+
// which silently consumes it and auto-continues. This branch counts the same
|
|
433
|
+
// queue as cancellable work, and a streaming turn always has the indicator
|
|
434
|
+
// mounted, so without this hand-off it short-circuited first and turned the
|
|
435
|
+
// documented steer-on-interrupt into a loud abort that dumped the steer back
|
|
436
|
+
// into the composer. `cancelPendingSubmission()` above still wins: a
|
|
437
|
+
// submission that has not started yet is cancelled before the stream owns
|
|
438
|
+
// the key, and one that has started returns false there by design.
|
|
439
|
+
if (
|
|
440
|
+
options.streaming === true &&
|
|
441
|
+
this.ctx.session.isStreaming &&
|
|
442
|
+
this.ctx.session.hasQueuedSteering &&
|
|
443
|
+
!this.ctx.session.isBashRunning &&
|
|
444
|
+
!this.ctx.session.isEvalRunning &&
|
|
445
|
+
!this.#steerConsumePending
|
|
446
|
+
) {
|
|
447
|
+
this.#steerConsumePending = true;
|
|
448
|
+
void this.#abortInteractive({ silent: true });
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
423
451
|
// Only count queues this handler can actually drain. The aggregate
|
|
424
452
|
// `queuedMessageCount` also counts hidden next-turn context, which
|
|
425
453
|
// `restoreQueuedMessagesToEditor()` never returns and which survives turn
|