@gajae-code/coding-agent 0.15.6 → 0.16.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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -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 +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- 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/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -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/main.d.ts +1 -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/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- 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/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -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/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -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/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- 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 +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- 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/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- 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 +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- 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/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- 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 +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- 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 +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- 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/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- 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 +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -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 +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- 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 +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- 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/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -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"
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
normalizeTierMap,
|
|
23
23
|
validateAutoroutingSetup,
|
|
24
24
|
} from "../../config/autorouting-contract";
|
|
25
|
+
import type { BillingPath } from "../../config/billing-path";
|
|
25
26
|
import {
|
|
26
27
|
getProxyRoutableProviders,
|
|
27
28
|
inspectProxyProviderId,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
rewriteSelectorForProxy,
|
|
31
32
|
tryResolveProxyProviderId,
|
|
32
33
|
} from "../../config/model-profile-activation";
|
|
33
|
-
|
|
34
34
|
import { isModelProfileProviderAvailable } from "../../config/model-profile-contract";
|
|
35
35
|
import {
|
|
36
36
|
deriveModelProfileMappedProviders,
|
|
@@ -429,6 +429,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
429
429
|
#imageRoleFilter: boolean = false;
|
|
430
430
|
#smartRoutingPanel?: SmartRoutingPanelComponent;
|
|
431
431
|
#smartRoutingPreviewBuilder?: (draft: AutoroutingSetup) => SmartRoutingPreview;
|
|
432
|
+
#isBillingDismissed?: (key: string) => boolean;
|
|
433
|
+
#markBillingDismissed?: (key: string) => void;
|
|
434
|
+
#pendingBillingNoticeKey?: string;
|
|
432
435
|
|
|
433
436
|
// Tab state
|
|
434
437
|
#providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
|
|
@@ -455,6 +458,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
455
458
|
smartRoutingPreview?: (draft: AutoroutingSetup) => SmartRoutingPreview;
|
|
456
459
|
/** Open the smart-routing panel directly instead of the preset landing. */
|
|
457
460
|
smartRoutingOnly?: boolean;
|
|
461
|
+
/** Test seam: synchronous persisted-dismissal check to avoid async render churn. */
|
|
462
|
+
isBillingDismissed?: (key: string) => boolean;
|
|
463
|
+
/** Test seam: record that a billing notice was shown/dismissed. */
|
|
464
|
+
markBillingDismissed?: (key: string) => void;
|
|
458
465
|
},
|
|
459
466
|
) {
|
|
460
467
|
super();
|
|
@@ -468,6 +475,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
468
475
|
this.#temporaryOnly = options?.temporaryOnly ?? false;
|
|
469
476
|
this.#authSessionId = options?.sessionId;
|
|
470
477
|
this.#smartRoutingPreviewBuilder = options?.smartRoutingPreview;
|
|
478
|
+
this.#isBillingDismissed = (options as any)?.isBillingDismissed;
|
|
479
|
+
this.#markBillingDismissed = (options as any)?.markBillingDismissed;
|
|
471
480
|
this.#currentModel = _currentModel;
|
|
472
481
|
this.#currentThinkingLevel = options?.currentThinkingLevel;
|
|
473
482
|
this.#activeModelProfile = options?.activeModelProfile;
|
|
@@ -1497,6 +1506,25 @@ export class ModelSelectorComponent extends Container {
|
|
|
1497
1506
|
const availableModels = this.#getProfileAvailableModels();
|
|
1498
1507
|
const resolutionRegistry = this.#createProfileResolutionRegistry(availableModels);
|
|
1499
1508
|
const providers = new Set<string>();
|
|
1509
|
+
// Preset groups share selectors heavily (role assignments repeat across
|
|
1510
|
+
// profiles), and each `resolveModelRoleValue` call scans the candidate set
|
|
1511
|
+
// while validating lookup contexts and resolving aliases. Resolve identical
|
|
1512
|
+
// selectors once per refresh; the inputs are shared and only the provider is
|
|
1513
|
+
// needed here. This map is deliberately local to the synchronous, pre-await
|
|
1514
|
+
// snapshot below: catalog/config changes and credential-generation changes
|
|
1515
|
+
// start a new refresh with a new map.
|
|
1516
|
+
const resolvedProviderBySelector = new Map<string, string | undefined>();
|
|
1517
|
+
const resolveSelectorProvider = (selector: string): string | undefined => {
|
|
1518
|
+
if (resolvedProviderBySelector.has(selector)) return resolvedProviderBySelector.get(selector);
|
|
1519
|
+
const provider = resolveModelRoleValue(selector, availableModels, {
|
|
1520
|
+
settings: this.#settings,
|
|
1521
|
+
modelRegistry: this.#modelRegistry,
|
|
1522
|
+
aliasIntent: "preset-equivalent",
|
|
1523
|
+
credentialSessionId: this.#authSessionId,
|
|
1524
|
+
}).model?.provider;
|
|
1525
|
+
resolvedProviderBySelector.set(selector, provider);
|
|
1526
|
+
return provider;
|
|
1527
|
+
};
|
|
1500
1528
|
for (const profiles of this.#getPresetGroups().values()) {
|
|
1501
1529
|
for (const profile of profiles) {
|
|
1502
1530
|
for (const provider of profileRequiredProviders(profile)) providers.add(provider);
|
|
@@ -1516,13 +1544,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1516
1544
|
];
|
|
1517
1545
|
for (const value of values) {
|
|
1518
1546
|
for (const selector of normalizeModelSelectorValue(value)) {
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
modelRegistry: this.#modelRegistry,
|
|
1522
|
-
aliasIntent: "preset-equivalent",
|
|
1523
|
-
credentialSessionId: this.#authSessionId,
|
|
1524
|
-
}).model;
|
|
1525
|
-
if (resolved) providers.add(resolved.provider);
|
|
1547
|
+
const resolvedProvider = resolveSelectorProvider(selector);
|
|
1548
|
+
if (resolvedProvider !== undefined) providers.add(resolvedProvider);
|
|
1526
1549
|
}
|
|
1527
1550
|
}
|
|
1528
1551
|
}
|
|
@@ -1705,12 +1728,24 @@ export class ModelSelectorComponent extends Container {
|
|
|
1705
1728
|
return lines;
|
|
1706
1729
|
}
|
|
1707
1730
|
|
|
1731
|
+
#billingPathForModel(model: Model): BillingPath | undefined {
|
|
1732
|
+
try {
|
|
1733
|
+
const fn = (
|
|
1734
|
+
this.#modelRegistry as unknown as { getBillingPath?: (m: Model, s?: string) => BillingPath | undefined }
|
|
1735
|
+
).getBillingPath;
|
|
1736
|
+
if (typeof fn === "function") return fn.call(this.#modelRegistry, model, this.#authSessionId);
|
|
1737
|
+
} catch {}
|
|
1738
|
+
return undefined;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1708
1741
|
#formatAssignedModelLabel(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1709
1742
|
const modelLabel = sanitizeText(`${model.provider}/${model.id}`).replace(/\s+/g, " ").trim();
|
|
1710
1743
|
let label = modelLabel;
|
|
1711
1744
|
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
|
1712
1745
|
label += ` (${getThinkingLevelMetadata(thinkingLevel).label})`;
|
|
1713
1746
|
}
|
|
1747
|
+
const billing = this.#billingPathForModel(model)?.label;
|
|
1748
|
+
if (billing) label += ` [${billing}]`;
|
|
1714
1749
|
return truncateToWidth(label, ROLE_BINDING_MAX_WIDTH);
|
|
1715
1750
|
}
|
|
1716
1751
|
|
|
@@ -1914,6 +1949,27 @@ export class ModelSelectorComponent extends Container {
|
|
|
1914
1949
|
this.#listContainer.addChild(new Spacer(1));
|
|
1915
1950
|
this.#listContainer.addChild(new Text(theme.fg("warning", ` ${this.#presetLoginHint}`), 0, 0));
|
|
1916
1951
|
}
|
|
1952
|
+
// One-time metered billing notice for the currently selected model (first bind per credential source)
|
|
1953
|
+
try {
|
|
1954
|
+
const sel = this.#getSelectedItem?.() as any;
|
|
1955
|
+
const m = sel?.model as any;
|
|
1956
|
+
if (m && this.#viewMode === "models") {
|
|
1957
|
+
const bp = this.#billingPathForModel(m);
|
|
1958
|
+
if (bp && !(this.#isBillingDismissed?.(bp.dismissKey) ?? false)) {
|
|
1959
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
1960
|
+
this.#listContainer.addChild(
|
|
1961
|
+
new Text(
|
|
1962
|
+
theme.fg("warning", ` Billing: ${bp.label} — charges may apply. Press d to dismiss.`),
|
|
1963
|
+
0,
|
|
1964
|
+
0,
|
|
1965
|
+
),
|
|
1966
|
+
);
|
|
1967
|
+
this.#pendingBillingNoticeKey = bp.dismissKey;
|
|
1968
|
+
} else {
|
|
1969
|
+
this.#pendingBillingNoticeKey = undefined;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
} catch {}
|
|
1917
1973
|
const previewProfile = this.#getProfileByName(this.#previewProfileName);
|
|
1918
1974
|
if (previewProfile) this.#renderPresetPreview(previewProfile);
|
|
1919
1975
|
}
|
|
@@ -2075,6 +2131,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
2075
2131
|
) {
|
|
2076
2132
|
roleBadgeTokens.push(theme.icon.fast);
|
|
2077
2133
|
}
|
|
2134
|
+
const billingLabel = this.#billingPathForModel(item.model)?.label;
|
|
2135
|
+
if (billingLabel) roleBadgeTokens.push(theme.fg("warning", `[${billingLabel}]`));
|
|
2078
2136
|
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
|
|
2079
2137
|
|
|
2080
2138
|
let line = "";
|
|
@@ -2307,6 +2365,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
2307
2365
|
return;
|
|
2308
2366
|
}
|
|
2309
2367
|
|
|
2368
|
+
if (keyData === "d" || keyData === "D") {
|
|
2369
|
+
if (this.#pendingBillingNoticeKey) {
|
|
2370
|
+
try {
|
|
2371
|
+
this.#markBillingDismissed?.(this.#pendingBillingNoticeKey);
|
|
2372
|
+
} catch {}
|
|
2373
|
+
this.#pendingBillingNoticeKey = undefined;
|
|
2374
|
+
this.#updateList();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2310
2378
|
// Pass everything else to search input
|
|
2311
2379
|
this.#searchInput.handleInput(keyData);
|
|
2312
2380
|
this.#filterModels(this.#searchInput.getValue());
|
|
@@ -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;
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
import { formatBytes } from "@gajae-code/utils";
|
|
15
15
|
import { theme } from "../../modes/theme/theme";
|
|
16
16
|
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
prioritizeStarredSessions,
|
|
19
|
+
type ResumeSessionIdentity,
|
|
20
|
+
type ResumeTailInspection,
|
|
21
|
+
type SessionInfo,
|
|
22
|
+
} from "../../session/session-manager";
|
|
18
23
|
import { DynamicBorder } from "./dynamic-border";
|
|
19
24
|
import { HookSelectorComponent } from "./hook-selector";
|
|
20
25
|
|
|
@@ -71,17 +76,19 @@ class SessionList implements Component {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
#filterSessions(query: string): void {
|
|
74
|
-
this.#filteredSessions =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
this.#filteredSessions = prioritizeStarredSessions(
|
|
80
|
+
fuzzyFilter(this.allSessions, query, session =>
|
|
81
|
+
[
|
|
82
|
+
session.id,
|
|
83
|
+
session.title ?? "",
|
|
84
|
+
session.cwd ?? "",
|
|
85
|
+
session.firstMessage ?? "",
|
|
86
|
+
session.allMessagesText,
|
|
87
|
+
session.path,
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" "),
|
|
91
|
+
),
|
|
85
92
|
);
|
|
86
93
|
this.#clampSelectedIndex();
|
|
87
94
|
}
|
|
@@ -131,8 +138,9 @@ class SessionList implements Component {
|
|
|
131
138
|
const cursorWidth = visibleWidth(cursor);
|
|
132
139
|
const prefix = selected ? theme.fg("accent", cursor) : padding(cursorWidth);
|
|
133
140
|
const message = session.firstMessage.replace(/\n/g, " ").trim();
|
|
134
|
-
const
|
|
135
|
-
|
|
141
|
+
const star = session.starred ? theme.fg("warning", `${theme.icon.star} `) : "";
|
|
142
|
+
const title = truncateToWidth(session.title ?? message, width - cursorWidth - visibleWidth(star));
|
|
143
|
+
lines.push(prefix + star + (selected ? theme.bold(title) : title));
|
|
136
144
|
if (session.title) lines.push(` ${theme.fg("dim", truncateToWidth(message, width - cursorWidth))}`);
|
|
137
145
|
lines.push(
|
|
138
146
|
theme.fg(
|
|
@@ -211,7 +219,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
211
219
|
this.addChild(new DynamicBorder());
|
|
212
220
|
this.addChild(new Spacer(1));
|
|
213
221
|
this.addChild(this.#messageContainer);
|
|
214
|
-
this.#sessionList = new SessionList(sessions);
|
|
222
|
+
this.#sessionList = new SessionList(prioritizeStarredSessions(sessions));
|
|
215
223
|
this.#sessionList.onSelect = session => {
|
|
216
224
|
if (this.inspector) void this.#inspect(session);
|
|
217
225
|
else this.#dispatchSelect(session.path);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { type Component, Container, matchesKey, truncateToWidth } from "@gajae-code/tui";
|
|
3
|
-
import type
|
|
2
|
+
import { type Component, Container, matchesKey, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
3
|
+
import { prioritizeStarredSessions, type SessionInfo } from "../../session/session-manager";
|
|
4
4
|
import { theme } from "../theme/theme";
|
|
5
5
|
|
|
6
6
|
const PRESENCE_MAX_BYTES = 4096;
|
|
@@ -41,6 +41,7 @@ export interface DashboardSession {
|
|
|
41
41
|
id: string;
|
|
42
42
|
cwd: string;
|
|
43
43
|
title: string;
|
|
44
|
+
starred: boolean;
|
|
44
45
|
modified: Date;
|
|
45
46
|
messageCount: number;
|
|
46
47
|
messageCountIsEstimate?: boolean;
|
|
@@ -78,10 +79,11 @@ export function dashboardSessions(
|
|
|
78
79
|
sessions: readonly SessionInfo[],
|
|
79
80
|
options: { readFile?: (filePath: string) => string; now?: number } = {},
|
|
80
81
|
): DashboardSession[] {
|
|
81
|
-
return sessions.map(session => ({
|
|
82
|
+
return prioritizeStarredSessions(sessions).map(session => ({
|
|
82
83
|
id: session.id,
|
|
83
84
|
cwd: session.cwd,
|
|
84
85
|
title: session.title ?? session.firstMessage,
|
|
86
|
+
starred: session.starred,
|
|
85
87
|
modified: session.modified,
|
|
86
88
|
messageCountIsEstimate: session.messageCountIsEstimate,
|
|
87
89
|
messageCount: session.messageCount,
|
|
@@ -169,10 +171,11 @@ class SessionDashboardRow implements Component {
|
|
|
169
171
|
const fullMetadata = `· ${count} · ${modified}`;
|
|
170
172
|
const compactMetadata = `· ${this.session.messageCountIsEstimate ? `~${this.session.messageCount}` : this.session.messageCount} msgs`;
|
|
171
173
|
const metadata = width >= 48 ? fullMetadata : width >= 24 ? compactMetadata : "";
|
|
172
|
-
const
|
|
174
|
+
const star = this.session.starred ? theme.fg("warning", `${theme.icon.star} `) : "";
|
|
175
|
+
const titleWidth = Math.max(0, width - 2 - visibleWidth(star) - metadata.length - (metadata ? 1 : 0));
|
|
173
176
|
return [
|
|
174
177
|
fit(
|
|
175
|
-
` ${status} ${truncateToWidth(title, titleWidth)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}`,
|
|
178
|
+
` ${status} ${star}${truncateToWidth(title, titleWidth)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}`,
|
|
176
179
|
width,
|
|
177
180
|
),
|
|
178
181
|
fit(` ${theme.fg("dim", cwd)}`, width),
|
|
@@ -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 = {
|