@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.2
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 +92 -20
- package/dist/cli.js +3485 -3448
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +42 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +139 -54
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +221 -42
- package/src/session/agent-session.ts +1285 -348
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +48 -0
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +7 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
visibleWidth,
|
|
29
29
|
} from "@oh-my-pi/pi-tui";
|
|
30
30
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
31
|
+
import { type ModelRoleLookup, type ResolvedModelRoleValue, resolveModelRoleValue } from "../../config/model-resolver";
|
|
31
32
|
import { getKnownRoleIds, getRoleInfo } from "../../config/model-roles";
|
|
32
33
|
import type { Settings } from "../../config/settings";
|
|
33
34
|
import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../../thinking";
|
|
@@ -75,11 +76,19 @@ export interface ScopedModelItem {
|
|
|
75
76
|
thinkingLevel?: string;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
export type ModelRoleSelectionScope = "global" | "project";
|
|
80
|
+
|
|
78
81
|
export interface ModelHubCallbacks {
|
|
79
82
|
/** Persist a role assignment. */
|
|
80
|
-
onAssign: (
|
|
83
|
+
onAssign: (
|
|
84
|
+
model: Model,
|
|
85
|
+
role: string,
|
|
86
|
+
thinkingLevel: ConfiguredThinkingLevel | undefined,
|
|
87
|
+
selector: string,
|
|
88
|
+
scope?: ModelRoleSelectionScope,
|
|
89
|
+
) => void;
|
|
81
90
|
/** Clear a configured role back to auto-selection. */
|
|
82
|
-
onUnassign: (role: string) => void;
|
|
91
|
+
onUnassign: (role: string, scope?: ModelRoleSelectionScope) => void;
|
|
83
92
|
/** Persist a `retry.fallbackChains` entry — keyed by a role, `provider/model-id`, or `provider/*`; an empty chain clears the key. */
|
|
84
93
|
onFallbackChainChange?: (role: string, chain: string[]) => void;
|
|
85
94
|
/** Locked provider activation: forward to the /login flow. */
|
|
@@ -111,18 +120,20 @@ interface StripChip {
|
|
|
111
120
|
/** Pre-styled label body (without selection decoration). */
|
|
112
121
|
styled: string;
|
|
113
122
|
role?: string;
|
|
114
|
-
action: "assign" | "unassign" | "fallback" | "fallbackModel" | "fallbackProvider" | "thinking";
|
|
123
|
+
action: "assign" | "unassign" | "fallback" | "fallbackModel" | "fallbackProvider" | "scope" | "thinking";
|
|
115
124
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
125
|
+
scope?: ModelRoleSelectionScope;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
type StripState =
|
|
119
129
|
| {
|
|
120
|
-
kind: "role" | "thinking";
|
|
130
|
+
kind: "role" | "scope" | "thinking";
|
|
121
131
|
item: ModelBrowserItem;
|
|
122
132
|
role?: string;
|
|
133
|
+
scope?: ModelRoleSelectionScope;
|
|
123
134
|
chips: StripChip[];
|
|
124
135
|
index: number;
|
|
125
|
-
/** Where to land when a thinking strip closes. */
|
|
136
|
+
/** Where to land when a scope or thinking strip closes. */
|
|
126
137
|
returnToRoles: boolean;
|
|
127
138
|
}
|
|
128
139
|
| {
|
|
@@ -556,6 +567,11 @@ export class ModelHubComponent implements Component {
|
|
|
556
567
|
this.#tui.requestRender();
|
|
557
568
|
}
|
|
558
569
|
|
|
570
|
+
/** Re-sync after an asynchronous callback finishes mutating settings. */
|
|
571
|
+
refreshAfterExternalMutation(): void {
|
|
572
|
+
this.#refreshAfterMutation();
|
|
573
|
+
}
|
|
574
|
+
|
|
559
575
|
/**
|
|
560
576
|
* Recompute per-provider match counts for the active query. Providers
|
|
561
577
|
* without matches gray out and the scope hop skips them; a provider scope
|
|
@@ -745,23 +761,55 @@ export class ModelHubComponent implements Component {
|
|
|
745
761
|
this.#openRoleStrip(item);
|
|
746
762
|
}
|
|
747
763
|
|
|
764
|
+
#roleForScope(role: string, scope: ModelRoleSelectionScope): ResolvedModelRoleValue {
|
|
765
|
+
const roleValue =
|
|
766
|
+
scope === "project" ? this.#settings.getProjectModelRole(role) : this.#settings.getGlobalModelRole(role);
|
|
767
|
+
const allModels =
|
|
768
|
+
this.#scopedModels.length > 0 ? this.#scopedModels.map(scoped => scoped.model) : this.#registry.getAll();
|
|
769
|
+
const roleLookup: ModelRoleLookup = {
|
|
770
|
+
getModelRole: scopedRole =>
|
|
771
|
+
scope === "project"
|
|
772
|
+
? (this.#settings.getProjectModelRole(scopedRole) ?? this.#settings.getGlobalModelRole(scopedRole))
|
|
773
|
+
: this.#settings.getGlobalModelRole(scopedRole),
|
|
774
|
+
};
|
|
775
|
+
return resolveModelRoleValue(roleValue, allModels, { settings: this.#settings, roleLookup });
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#thinkingLevelForScope(role: string, scope: ModelRoleSelectionScope): ConfiguredThinkingLevel {
|
|
779
|
+
const resolved = this.#roleForScope(role, scope);
|
|
780
|
+
return resolved.explicitThinkingLevel ? (resolved.thinkingLevel ?? ThinkingLevel.Inherit) : ThinkingLevel.Inherit;
|
|
781
|
+
}
|
|
782
|
+
|
|
748
783
|
/** Persist `role → item`, preserving a still-supported thinking level, then open the thinking strip. */
|
|
749
|
-
#assignRole(item: ModelBrowserItem, role: string, returnToRoles: boolean): void {
|
|
784
|
+
#assignRole(item: ModelBrowserItem, role: string, returnToRoles: boolean, scope?: ModelRoleSelectionScope): void {
|
|
785
|
+
if (this.#settings.get("modelRoleStorage") === "project" && scope === undefined) {
|
|
786
|
+
this.#openScopeStrip(item, role, returnToRoles);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
750
790
|
const current = this.#roles[role];
|
|
751
791
|
let level: ConfiguredThinkingLevel = ThinkingLevel.Inherit;
|
|
752
|
-
if (
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
792
|
+
if (this.#settings.get("modelRoleStorage") === "project" && scope !== undefined) {
|
|
793
|
+
level = this.#thinkingLevelForScope(role, scope);
|
|
794
|
+
} else if (current && !current.autoSelected) {
|
|
795
|
+
level = current.thinkingLevel;
|
|
796
|
+
}
|
|
797
|
+
const supported = this.#thinkingOptionsFor(item.model);
|
|
798
|
+
if (!supported.includes(level)) level = ThinkingLevel.Inherit;
|
|
799
|
+
this.#callbacks.onAssign(item.model, role, level, item.selector, scope);
|
|
757
800
|
this.#refreshAfterMutation();
|
|
758
|
-
this.#openThinkingStrip(item, role, returnToRoles);
|
|
801
|
+
this.#openThinkingStrip(item, role, returnToRoles, scope);
|
|
759
802
|
}
|
|
760
803
|
|
|
761
804
|
#unassignRole(role: string): void {
|
|
762
805
|
const assignment = this.#roles[role];
|
|
763
806
|
if (!assignment || assignment.autoSelected) return;
|
|
764
|
-
this.#
|
|
807
|
+
if (this.#settings.get("modelRoleStorage") === "project") {
|
|
808
|
+
const source = this.#settings.getModelRoleSource(role);
|
|
809
|
+
this.#callbacks.onUnassign(role, source === "default" ? undefined : source);
|
|
810
|
+
} else {
|
|
811
|
+
this.#callbacks.onUnassign(role);
|
|
812
|
+
}
|
|
765
813
|
this.#refreshAfterMutation();
|
|
766
814
|
}
|
|
767
815
|
|
|
@@ -771,24 +819,32 @@ export class ModelHubComponent implements Component {
|
|
|
771
819
|
|
|
772
820
|
#openRoleStrip(item: ModelBrowserItem): void {
|
|
773
821
|
const chips: StripChip[] = [];
|
|
822
|
+
const scopedStorage = this.#settings.get("modelRoleStorage") === "project";
|
|
823
|
+
const scopes: readonly ModelRoleSelectionScope[] = scopedStorage ? ["project", "global"] : ["global"];
|
|
774
824
|
for (const role of this.#visibleRoleIds()) {
|
|
775
825
|
const info = getRoleInfo(role, this.#settings);
|
|
776
826
|
const assignment = this.#roles[role];
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
827
|
+
for (const scope of scopes) {
|
|
828
|
+
const scopedModel = scopedStorage
|
|
829
|
+
? this.#roleForScope(role, scope).model
|
|
830
|
+
: assignment && !assignment.autoSelected
|
|
831
|
+
? assignment.model
|
|
832
|
+
: undefined;
|
|
833
|
+
const assignedHere =
|
|
834
|
+
!!scopedModel && scopedModel.provider === item.model.provider && scopedModel.id === item.model.id;
|
|
835
|
+
const roleLabel = (info.tag ?? info.name ?? role).toLowerCase();
|
|
836
|
+
const label = scopedStorage ? `${scope} ${roleLabel}` : roleLabel;
|
|
837
|
+
chips.push({
|
|
838
|
+
label,
|
|
839
|
+
styled: assignedHere
|
|
840
|
+
? theme.fg(info.color ?? "muted", `${theme.status.enabled}${label}`) +
|
|
841
|
+
theme.fg("dim", ` ${theme.status.success}`)
|
|
842
|
+
: theme.fg(info.color ?? "muted", label),
|
|
843
|
+
role,
|
|
844
|
+
scope,
|
|
845
|
+
action: assignedHere ? "unassign" : "assign",
|
|
846
|
+
});
|
|
847
|
+
}
|
|
792
848
|
}
|
|
793
849
|
chips.push({
|
|
794
850
|
label: `fallbacks:${item.model.id}`,
|
|
@@ -804,9 +860,25 @@ export class ModelHubComponent implements Component {
|
|
|
804
860
|
this.#strip = { kind: "role", item, chips, index: 0, returnToRoles: false };
|
|
805
861
|
}
|
|
806
862
|
|
|
807
|
-
#
|
|
863
|
+
#openScopeStrip(item: ModelBrowserItem, role: string, returnToRoles: boolean): void {
|
|
864
|
+
const chips: StripChip[] = [
|
|
865
|
+
{ label: "project", styled: theme.fg("accent", "project"), action: "scope", scope: "project" },
|
|
866
|
+
{ label: "global", styled: theme.fg("muted", "global"), action: "scope", scope: "global" },
|
|
867
|
+
];
|
|
868
|
+
this.#strip = { kind: "scope", item, role, chips, index: 0, returnToRoles };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
#openThinkingStrip(
|
|
872
|
+
item: ModelBrowserItem,
|
|
873
|
+
role: string,
|
|
874
|
+
returnToRoles: boolean,
|
|
875
|
+
scope?: ModelRoleSelectionScope,
|
|
876
|
+
): void {
|
|
808
877
|
const options = this.#thinkingOptionsFor(item.model);
|
|
809
|
-
const current =
|
|
878
|
+
const current =
|
|
879
|
+
this.#settings.get("modelRoleStorage") === "project" && scope !== undefined
|
|
880
|
+
? this.#thinkingLevelForScope(role, scope)
|
|
881
|
+
: (this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit);
|
|
810
882
|
const chips: StripChip[] = options.map(level => {
|
|
811
883
|
const label = getConfiguredThinkingLevelMetadata(level).label;
|
|
812
884
|
const glyph = thinkingLevelGlyph(level);
|
|
@@ -822,6 +894,7 @@ export class ModelHubComponent implements Component {
|
|
|
822
894
|
kind: "thinking",
|
|
823
895
|
item,
|
|
824
896
|
role,
|
|
897
|
+
scope,
|
|
825
898
|
chips,
|
|
826
899
|
index: preselect >= 0 ? preselect : 0,
|
|
827
900
|
returnToRoles,
|
|
@@ -832,7 +905,7 @@ export class ModelHubComponent implements Component {
|
|
|
832
905
|
const strip = this.#strip;
|
|
833
906
|
this.#strip = null;
|
|
834
907
|
this.#chipRanges = [];
|
|
835
|
-
if (strip?.kind === "thinking" && strip.returnToRoles) {
|
|
908
|
+
if ((strip?.kind === "scope" || strip?.kind === "thinking") && strip.returnToRoles) {
|
|
836
909
|
this.#setActiveEntry("roles");
|
|
837
910
|
this.#focus = "list";
|
|
838
911
|
}
|
|
@@ -847,12 +920,16 @@ export class ModelHubComponent implements Component {
|
|
|
847
920
|
case "assign":
|
|
848
921
|
if (chip.role) {
|
|
849
922
|
this.#strip = null;
|
|
850
|
-
this.#assignRole(strip.item, chip.role, false);
|
|
923
|
+
this.#assignRole(strip.item, chip.role, false, chip.scope);
|
|
851
924
|
}
|
|
852
925
|
return;
|
|
853
926
|
case "unassign":
|
|
854
927
|
if (chip.role) {
|
|
855
|
-
this.#
|
|
928
|
+
if (this.#settings.get("modelRoleStorage") === "project") {
|
|
929
|
+
this.#callbacks.onUnassign(chip.role, chip.scope);
|
|
930
|
+
} else {
|
|
931
|
+
this.#callbacks.onUnassign(chip.role);
|
|
932
|
+
}
|
|
856
933
|
this.#refreshAfterMutation();
|
|
857
934
|
}
|
|
858
935
|
this.#closeStrip();
|
|
@@ -869,9 +946,21 @@ export class ModelHubComponent implements Component {
|
|
|
869
946
|
this.#closeStrip();
|
|
870
947
|
this.#startAssignFallback(`${strip.item.model.provider}/*`, null);
|
|
871
948
|
return;
|
|
949
|
+
case "scope":
|
|
950
|
+
if (strip.role && chip.scope) {
|
|
951
|
+
this.#strip = null;
|
|
952
|
+
this.#assignRole(strip.item, strip.role, strip.returnToRoles, chip.scope);
|
|
953
|
+
}
|
|
954
|
+
return;
|
|
872
955
|
case "thinking":
|
|
873
956
|
if (strip.role && chip.thinkingLevel !== undefined) {
|
|
874
|
-
this.#callbacks.onAssign(
|
|
957
|
+
this.#callbacks.onAssign(
|
|
958
|
+
strip.item.model,
|
|
959
|
+
strip.role,
|
|
960
|
+
chip.thinkingLevel,
|
|
961
|
+
strip.item.selector,
|
|
962
|
+
strip.scope,
|
|
963
|
+
);
|
|
875
964
|
this.#refreshAfterMutation();
|
|
876
965
|
}
|
|
877
966
|
this.#closeStrip();
|
|
@@ -1305,13 +1394,20 @@ export class ModelHubComponent implements Component {
|
|
|
1305
1394
|
if (printable === "t") {
|
|
1306
1395
|
const assignment = role ? this.#roles[role] : undefined;
|
|
1307
1396
|
if (role && assignment) {
|
|
1397
|
+
const source =
|
|
1398
|
+
this.#settings.get("modelRoleStorage") === "project"
|
|
1399
|
+
? this.#settings.getModelRoleSource(role)
|
|
1400
|
+
: "default";
|
|
1401
|
+
const scope = source === "project" || source === "global" ? source : undefined;
|
|
1402
|
+
const scopedModel = scope ? this.#roleForScope(role, scope).model : assignment.model;
|
|
1403
|
+
if (!scopedModel) return;
|
|
1308
1404
|
const item: ModelBrowserItem = {
|
|
1309
|
-
provider:
|
|
1310
|
-
id:
|
|
1311
|
-
model:
|
|
1312
|
-
selector: `${
|
|
1405
|
+
provider: scopedModel.provider,
|
|
1406
|
+
id: scopedModel.id,
|
|
1407
|
+
model: scopedModel,
|
|
1408
|
+
selector: `${scopedModel.provider}/${scopedModel.id}`,
|
|
1313
1409
|
};
|
|
1314
|
-
this.#openThinkingStrip(item, role, true);
|
|
1410
|
+
this.#openThinkingStrip(item, role, true, scope);
|
|
1315
1411
|
}
|
|
1316
1412
|
return;
|
|
1317
1413
|
}
|
|
@@ -1770,9 +1866,9 @@ export class ModelHubComponent implements Component {
|
|
|
1770
1866
|
if (strip.kind === "roleName") {
|
|
1771
1867
|
return "Enter create + pick model · Esc cancel";
|
|
1772
1868
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1869
|
+
if (strip.kind === "role") return "←/→ choose · Enter assign/clear · Esc cancel";
|
|
1870
|
+
if (strip.kind === "scope") return "←/→ save scope · Enter choose · Esc cancel";
|
|
1871
|
+
return "←/→ thinking level · Enter apply · Esc keep";
|
|
1776
1872
|
}
|
|
1777
1873
|
if (this.#assigning !== null) {
|
|
1778
1874
|
switch (this.#assigning.kind) {
|
|
@@ -879,6 +879,10 @@ export class SessionSelectorComponent extends Container {
|
|
|
879
879
|
lockInput(): void {
|
|
880
880
|
this.#inputLocked = true;
|
|
881
881
|
}
|
|
882
|
+
/** Re-enable input after a failed resume so the user can pick again. */
|
|
883
|
+
unlockInput(): void {
|
|
884
|
+
this.#inputLocked = false;
|
|
885
|
+
}
|
|
882
886
|
|
|
883
887
|
/**
|
|
884
888
|
* Dispose the session list explicitly: while the delete-confirmation dialog
|
|
@@ -36,6 +36,7 @@ function makeSessionWithLastMessage(lastMessage: unknown, prewalkArmed: boolean
|
|
|
36
36
|
getPrewalkState: () => (prewalkArmed ? { target: { id: "cheap-model", provider: "openai" } } : undefined),
|
|
37
37
|
getAsyncJobSnapshot: () => undefined,
|
|
38
38
|
isAdvisorActive: () => false,
|
|
39
|
+
getAdvisorStatusOverview: () => ({ configured: false, advisors: [] }),
|
|
39
40
|
isFastModeActive: () => false,
|
|
40
41
|
configuredThinkingLevel: () => undefined,
|
|
41
42
|
modelRegistry: {
|
|
@@ -129,9 +129,9 @@ const modelSegment: StatusLineSegment = {
|
|
|
129
129
|
|
|
130
130
|
// Fast-mode icon and thinking-level suffix trail the model name and are
|
|
131
131
|
// colored together with it as `statusLineModel`. The advisor "++" badge
|
|
132
|
-
// sits between the name and that tail
|
|
133
|
-
//
|
|
134
|
-
//
|
|
132
|
+
// sits between the name and that tail, so it reads as a distinct marker.
|
|
133
|
+
// theme.fg resets only the fg, so the spans are concatenated (not
|
|
134
|
+
// nested) to keep each color intact.
|
|
135
135
|
let tail = "";
|
|
136
136
|
if (ctx.session.isFastModeActive() && theme.icon.fast) {
|
|
137
137
|
tail += ` ${theme.icon.fast}`;
|
|
@@ -141,10 +141,25 @@ const modelSegment: StatusLineSegment = {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// `statusLineModel` is aliased to `accent` in many themes, so the badge
|
|
144
|
-
// uses
|
|
144
|
+
// uses status colors to stay visibly distinct from the model name color.
|
|
145
145
|
let content = theme.fg("statusLineModel", withIcon(modelIcon, modelName));
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
// Advisor "++" badge, colored by the worst status in the roster:
|
|
147
|
+
// success = all running, warning = quota-exhausted, error = failed,
|
|
148
|
+
// dim = everything paused/no-model. Per-advisor detail lives in
|
|
149
|
+
// `/advisor status`.
|
|
150
|
+
// Optional chaining: lightweight session doubles (test mocks) that don't
|
|
151
|
+
// implement getAdvisorStatusOverview skip the badge instead of crashing.
|
|
152
|
+
const advisorStats = ctx.session.getAdvisorStatusOverview?.();
|
|
153
|
+
if (advisorStats?.configured && advisorStats.advisors.length > 0) {
|
|
154
|
+
const statuses = advisorStats.advisors.map(a => a.status);
|
|
155
|
+
const badgeColor = statuses.includes("error")
|
|
156
|
+
? "error"
|
|
157
|
+
: statuses.includes("quota_exhausted")
|
|
158
|
+
? "warning"
|
|
159
|
+
: statuses.includes("running")
|
|
160
|
+
? "success"
|
|
161
|
+
: "dim";
|
|
162
|
+
content += theme.fg(badgeColor, "++");
|
|
148
163
|
}
|
|
149
164
|
if (tail) {
|
|
150
165
|
content += theme.fg("statusLineModel", tail);
|
|
@@ -43,7 +43,7 @@ import type { AuthStorage, OAuthAccountIdentity } from "../../session/auth-stora
|
|
|
43
43
|
import type { CompactMode } from "../../session/compact-modes";
|
|
44
44
|
import type { NewSessionOptions } from "../../session/session-entries";
|
|
45
45
|
import { formatShakeSummary, type ShakeMode, type ShakeResult } from "../../session/shake-types";
|
|
46
|
-
import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
46
|
+
import { formatActiveAccountLabel, limitMatchesActiveAccount } from "../../slash-commands/helpers/active-oauth-account";
|
|
47
47
|
import { outputMeta } from "../../tools/output-meta";
|
|
48
48
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
49
49
|
import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
|
|
@@ -349,46 +349,119 @@ export class CommandController {
|
|
|
349
349
|
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
static readonly #advisorStatusGlyph: Record<string, string> = {
|
|
353
|
+
running: "●",
|
|
354
|
+
paused: "○",
|
|
355
|
+
no_model: "○",
|
|
356
|
+
quota_exhausted: "✕",
|
|
357
|
+
error: "✕",
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
static readonly #advisorStatusLabel: Record<string, string> = {
|
|
361
|
+
running: "running",
|
|
362
|
+
paused: "off",
|
|
363
|
+
no_model: "no model",
|
|
364
|
+
quota_exhausted: "quota exhausted",
|
|
365
|
+
error: "error",
|
|
366
|
+
};
|
|
367
|
+
|
|
352
368
|
async handleAdvisorStatusCommand(): Promise<void> {
|
|
353
369
|
const stats = this.ctx.session.getAdvisorStats();
|
|
354
|
-
if (!stats.
|
|
355
|
-
this.ctx.present([
|
|
356
|
-
new Spacer(1),
|
|
357
|
-
new Text(
|
|
358
|
-
stats.configured
|
|
359
|
-
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
|
|
360
|
-
: "Advisor is disabled.",
|
|
361
|
-
1,
|
|
362
|
-
0,
|
|
363
|
-
),
|
|
364
|
-
]);
|
|
370
|
+
if (!stats.configured) {
|
|
371
|
+
this.ctx.present([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
|
|
365
372
|
return;
|
|
366
373
|
}
|
|
367
|
-
|
|
374
|
+
// Fetch live quota data (cached 5 min by the auth-gateway) so we can show
|
|
375
|
+
// real usage windows/reset timers per advisor provider. Non-fatal when absent.
|
|
376
|
+
const usageProvider = this.ctx.session as { fetchUsageReports?: () => Promise<UsageReport[] | null> };
|
|
377
|
+
let usageReports: UsageReport[] | null = null;
|
|
378
|
+
if (usageProvider.fetchUsageReports) {
|
|
379
|
+
try {
|
|
380
|
+
usageReports = await usageProvider.fetchUsageReports();
|
|
381
|
+
} catch {
|
|
382
|
+
// Network/auth failure is non-fatal — just skip the quota line.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Resolve the active OAuth identity for each advisor's provider so quota
|
|
386
|
+
// filtering matches the credential actually in use (not sibling accounts).
|
|
387
|
+
const resolveActiveAdvisorAccount = (provider: string, sessionId?: string): OAuthAccountIdentity | undefined =>
|
|
388
|
+
this.ctx.session.modelRegistry.authStorage.getOAuthAccountIdentity(
|
|
389
|
+
provider,
|
|
390
|
+
sessionId ?? this.ctx.session.sessionId,
|
|
391
|
+
);
|
|
392
|
+
const nowMs = Date.now();
|
|
393
|
+
// Roster view: show every configured advisor with its status, even when
|
|
394
|
+
// none are live (all paused/no-model). The old code returned a generic
|
|
395
|
+
// message that hid the per-advisor state the user needs to act on.
|
|
396
|
+
if (stats.advisors.length > 1 || (stats.configured && !stats.active)) {
|
|
368
397
|
let info = `${theme.bold("Advisor Status")} (${stats.advisors.length} advisors)\n`;
|
|
369
398
|
for (const a of stats.advisors) {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
info +=
|
|
379
|
-
if (a.
|
|
380
|
-
|
|
399
|
+
const glyph = CommandController.#advisorStatusGlyph[a.status] ?? "?";
|
|
400
|
+
const label = CommandController.#advisorStatusLabel[a.status] ?? a.status;
|
|
401
|
+
const color =
|
|
402
|
+
a.status === "running"
|
|
403
|
+
? "success"
|
|
404
|
+
: a.status === "quota_exhausted" || a.status === "error"
|
|
405
|
+
? "error"
|
|
406
|
+
: "dim";
|
|
407
|
+
info += `\n${theme.fg(color, glyph)} ${theme.bold(a.name)} ${theme.fg("dim", `[${label}]`)}\n`;
|
|
408
|
+
if (a.model) {
|
|
409
|
+
info += `${theme.fg("dim", "Model:")} ${a.model.provider}/${a.model.id}\n`;
|
|
410
|
+
}
|
|
411
|
+
if (a.model && usageReports) {
|
|
412
|
+
const quota = formatCompactQuota(
|
|
413
|
+
a.model.provider,
|
|
414
|
+
usageReports,
|
|
415
|
+
nowMs,
|
|
416
|
+
resolveActiveAdvisorAccount(a.model.provider, a.sessionId),
|
|
417
|
+
);
|
|
418
|
+
if (quota) info += `${theme.fg("dim", quota)}\n`;
|
|
419
|
+
}
|
|
420
|
+
if (a.status === "running" || a.status === "quota_exhausted") {
|
|
421
|
+
const ctx =
|
|
422
|
+
a.contextWindow > 0
|
|
423
|
+
? `${a.contextTokens.toLocaleString()} / ${a.contextWindow.toLocaleString()} (${Math.round((a.contextTokens / a.contextWindow) * 100)}%)`
|
|
424
|
+
: `${a.contextTokens.toLocaleString()}`;
|
|
425
|
+
info += `${theme.fg("dim", "Context:")} ${ctx}\n`;
|
|
426
|
+
info += `${theme.fg("dim", "Messages:")} ${a.messages.total.toLocaleString()}\n`;
|
|
427
|
+
info += `${theme.fg("dim", "Spend:")} ${a.tokens.input.toLocaleString()} in / ${a.tokens.output.toLocaleString()} out`;
|
|
428
|
+
if (a.cost > 0) info += `, $${a.cost.toFixed(4)}`;
|
|
429
|
+
info += "\n";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (stats.active) {
|
|
433
|
+
info += `\n${theme.bold("Totals")}\n`;
|
|
434
|
+
info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
435
|
+
if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
|
|
381
436
|
}
|
|
382
|
-
info += `\n${theme.bold("Totals")}\n`;
|
|
383
|
-
info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
384
|
-
if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
|
|
385
437
|
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
|
|
386
438
|
return;
|
|
387
439
|
}
|
|
388
|
-
|
|
440
|
+
// Single active advisor — detailed view.
|
|
441
|
+
const model = stats.model;
|
|
389
442
|
let info = `${theme.bold("Advisor Status")}\n\n`;
|
|
390
|
-
|
|
391
|
-
|
|
443
|
+
if (stats.advisors.length === 1) {
|
|
444
|
+
const a = stats.advisors[0];
|
|
445
|
+
const glyph = CommandController.#advisorStatusGlyph[a.status] ?? "?";
|
|
446
|
+
const label = CommandController.#advisorStatusLabel[a.status] ?? a.status;
|
|
447
|
+
info += `${theme.fg(a.status === "running" ? "success" : "error", glyph)} ${a.name} ${theme.fg("dim", `[${label}]`)}\n\n`;
|
|
448
|
+
}
|
|
449
|
+
if (model) {
|
|
450
|
+
info += `${theme.bold("Provider")}\n`;
|
|
451
|
+
info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
|
|
452
|
+
}
|
|
453
|
+
if (model && usageReports) {
|
|
454
|
+
const quota = formatCompactQuota(
|
|
455
|
+
model.provider,
|
|
456
|
+
usageReports,
|
|
457
|
+
nowMs,
|
|
458
|
+
resolveActiveAdvisorAccount(model.provider, stats.advisors[0]?.sessionId),
|
|
459
|
+
);
|
|
460
|
+
if (quota) {
|
|
461
|
+
info += `\n${theme.bold("Quota")}\n`;
|
|
462
|
+
info += `${theme.fg("dim", quota)}\n`;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
392
465
|
info += `\n${theme.bold("Messages")}\n`;
|
|
393
466
|
info += `${theme.fg("dim", "User:")} ${stats.messages.user.toLocaleString()}\n`;
|
|
394
467
|
info += `${theme.fg("dim", "Assistant:")} ${stats.messages.assistant.toLocaleString()}\n`;
|
|
@@ -406,14 +479,7 @@ export class CommandController {
|
|
|
406
479
|
if (stats.tokens.cacheRead > 0) {
|
|
407
480
|
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
408
481
|
}
|
|
409
|
-
if (stats.
|
|
410
|
-
info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
|
|
411
|
-
}
|
|
412
|
-
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
413
|
-
if (stats.cost > 0) {
|
|
414
|
-
info += `\n${theme.bold("Cost")}\n`;
|
|
415
|
-
info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(4)}\n`;
|
|
416
|
-
}
|
|
482
|
+
if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
|
|
417
483
|
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
|
|
418
484
|
}
|
|
419
485
|
|
|
@@ -993,6 +1059,12 @@ export class CommandController {
|
|
|
993
1059
|
return;
|
|
994
1060
|
}
|
|
995
1061
|
}
|
|
1062
|
+
try {
|
|
1063
|
+
await this.ctx.settings.flush();
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
this.ctx.showError(`Failed to save pending settings: ${err instanceof Error ? err.message : String(err)}`);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
996
1068
|
|
|
997
1069
|
try {
|
|
998
1070
|
await this.ctx.sessionManager.moveTo(resolvedPath);
|
|
@@ -1000,7 +1072,6 @@ export class CommandController {
|
|
|
1000
1072
|
this.ctx.showError(`Move failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1001
1073
|
return;
|
|
1002
1074
|
}
|
|
1003
|
-
|
|
1004
1075
|
await this.ctx.applyCwdChange(resolvedPath);
|
|
1005
1076
|
|
|
1006
1077
|
this.ctx.updateEditorBorderColor();
|
|
@@ -1303,7 +1374,7 @@ export class CommandController {
|
|
|
1303
1374
|
}
|
|
1304
1375
|
|
|
1305
1376
|
const BAR_WIDTH_MAX = 24;
|
|
1306
|
-
const
|
|
1377
|
+
const COLUMN_WIDTH_MIN = 4;
|
|
1307
1378
|
|
|
1308
1379
|
function renderJobLine(job: AsyncJobSnapshotItem, now: number): string {
|
|
1309
1380
|
const duration = formatDuration(Math.max(0, now - job.startTime));
|
|
@@ -1537,6 +1608,57 @@ function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null {
|
|
|
1537
1608
|
}
|
|
1538
1609
|
return `resets in ${formatDuration(minReset)}`;
|
|
1539
1610
|
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Compact one-line quota summary for a single advisor's provider.
|
|
1613
|
+
* Returns `null` when the provider has no usage data.
|
|
1614
|
+
* When `activeAccount` is provided, only limits matching that credential
|
|
1615
|
+
* are shown (mirrors `renderUsageReports`'s account-stickiness filtering).
|
|
1616
|
+
* Example output: `Quota: 7d window · 67% used · resets in 3.2d`
|
|
1617
|
+
*/
|
|
1618
|
+
export function formatCompactQuota(
|
|
1619
|
+
provider: string,
|
|
1620
|
+
reports: UsageReport[],
|
|
1621
|
+
nowMs: number,
|
|
1622
|
+
activeAccount?: OAuthAccountIdentity,
|
|
1623
|
+
): string | null {
|
|
1624
|
+
const providerReports = reports.filter(r => r.provider === provider);
|
|
1625
|
+
if (providerReports.length === 0) return null;
|
|
1626
|
+
// Group limits by window id so we show BOTH the 5-hour and 7-day windows
|
|
1627
|
+
// (or any other distinct windows the provider exposes). Within each window,
|
|
1628
|
+
// pick the highest used fraction across accounts — that's the most pressing.
|
|
1629
|
+
const byWindow = new Map<string, { limit: UsageLimit; fraction: number }>();
|
|
1630
|
+
for (const report of providerReports) {
|
|
1631
|
+
for (const limit of report.limits) {
|
|
1632
|
+
// Skip limits that belong to a different credential than the one
|
|
1633
|
+
// the advisor is actually using, so we don't alarm the user with
|
|
1634
|
+
// an exhausted account that isn't theirs.
|
|
1635
|
+
if (activeAccount && !limitMatchesActiveAccount(report, limit, activeAccount)) continue;
|
|
1636
|
+
const fraction = resolveUsedFraction(limit);
|
|
1637
|
+
if (fraction === undefined) continue;
|
|
1638
|
+
const key = limit.window?.id ?? limit.scope.windowId ?? "—";
|
|
1639
|
+
const existing = byWindow.get(key);
|
|
1640
|
+
if (!existing || fraction > existing.fraction) byWindow.set(key, { limit, fraction });
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (byWindow.size === 0) return null;
|
|
1644
|
+
// Sort windows by urgency (highest fraction first) so the most pressing
|
|
1645
|
+
// quota is always the first thing the user sees.
|
|
1646
|
+
const entries = [...byWindow.values()].sort((a, b) => b.fraction - a.fraction);
|
|
1647
|
+
const lines: string[] = [];
|
|
1648
|
+
for (const { limit, fraction } of entries) {
|
|
1649
|
+
const pct = Math.round(fraction * 100);
|
|
1650
|
+
const windowLabel = limit.window?.label ?? limit.scope.windowId ?? "—";
|
|
1651
|
+
// Include the limit label (account/tier) when it carries identity beyond
|
|
1652
|
+
// the window name, so the user can tell which credential's quota is shown.
|
|
1653
|
+
const identity = limit.label.trim();
|
|
1654
|
+
const header = identity && identity !== windowLabel ? `${windowLabel} (${identity})` : windowLabel;
|
|
1655
|
+
const parts = [`${header}: ${pct}% used`];
|
|
1656
|
+
const reset = resolveResetRange([limit], nowMs);
|
|
1657
|
+
if (reset) parts.push(reset);
|
|
1658
|
+
lines.push(parts.join(" · "));
|
|
1659
|
+
}
|
|
1660
|
+
return `Quota: ${lines.join(" │ ")}`;
|
|
1661
|
+
}
|
|
1540
1662
|
|
|
1541
1663
|
function resolveStatusIcon(status: UsageLimit["status"], uiTheme: typeof theme): string {
|
|
1542
1664
|
if (status === "exhausted") return uiTheme.fg("error", uiTheme.status.error);
|
|
@@ -1571,7 +1693,7 @@ function renderUsageBar(limit: UsageLimit, uiTheme: typeof theme, barWidth: numb
|
|
|
1571
1693
|
}
|
|
1572
1694
|
|
|
1573
1695
|
/**
|
|
1574
|
-
* Pick a per-column width so
|
|
1696
|
+
* Pick a per-account column width so the columns and trailing amount fit in `available`.
|
|
1575
1697
|
* Falls back to the minimum when the terminal is too narrow rather than wrapping.
|
|
1576
1698
|
*/
|
|
1577
1699
|
function resolveColumnWidth(count: number, available: number, trailing: number): number {
|
|
@@ -1580,10 +1702,7 @@ function resolveColumnWidth(count: number, available: number, trailing: number):
|
|
|
1580
1702
|
const gaps = count - 1;
|
|
1581
1703
|
const spaceForBars = available - indent - gaps - (trailing > 0 ? trailing + 1 : 0);
|
|
1582
1704
|
const ideal = Math.floor(spaceForBars / count);
|
|
1583
|
-
|
|
1584
|
-
const max = BAR_WIDTH_MAX;
|
|
1585
|
-
if (ideal < min) return min;
|
|
1586
|
-
if (ideal > max) return max;
|
|
1705
|
+
if (ideal < COLUMN_WIDTH_MIN) return COLUMN_WIDTH_MIN;
|
|
1587
1706
|
return ideal;
|
|
1588
1707
|
}
|
|
1589
1708
|
|
|
@@ -1642,7 +1761,7 @@ export function renderUsageReports(
|
|
|
1642
1761
|
}
|
|
1643
1762
|
|
|
1644
1763
|
lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
|
|
1645
|
-
const activeAccountLabel = activeAccount
|
|
1764
|
+
const activeAccountLabel = formatActiveAccountLabel(activeAccount);
|
|
1646
1765
|
if (activeAccountLabel) {
|
|
1647
1766
|
lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
|
|
1648
1767
|
}
|
|
@@ -1719,6 +1838,7 @@ export function renderUsageReports(
|
|
|
1719
1838
|
const sectionCount = renderableGroups.reduce((max, g) => Math.max(max, g.sortedLimits.length), 0);
|
|
1720
1839
|
const sectionTrailing = renderableGroups.reduce((max, g) => Math.max(max, visibleWidth(g.amountText)), 0);
|
|
1721
1840
|
const sectionColumnWidth = resolveColumnWidth(sectionCount, availableWidth, sectionTrailing);
|
|
1841
|
+
const sectionBarWidth = Math.min(sectionColumnWidth, BAR_WIDTH_MAX);
|
|
1722
1842
|
|
|
1723
1843
|
for (const { group, sortedLimits, sortedReports, amountText } of renderableGroups) {
|
|
1724
1844
|
const status = resolveAggregateStatus(sortedLimits);
|
|
@@ -1736,7 +1856,7 @@ export function renderUsageReports(
|
|
|
1736
1856
|
);
|
|
1737
1857
|
lines.push(` ${accountLabels.join(" ")}`.trimEnd());
|
|
1738
1858
|
const bars = sortedLimits.map(limit =>
|
|
1739
|
-
padColumn(renderUsageBar(limit, uiTheme,
|
|
1859
|
+
padColumn(renderUsageBar(limit, uiTheme, sectionBarWidth), sectionColumnWidth),
|
|
1740
1860
|
);
|
|
1741
1861
|
lines.push(` ${bars.join(" ")} ${amountText}`.trimEnd());
|
|
1742
1862
|
const resetText = sortedLimits.length <= 1 ? resolveResetRange(sortedLimits, nowMs) : null;
|