@gajae-code/coding-agent 0.7.11 → 0.8.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 +47 -0
- package/README.md +1 -1
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +25 -2
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line.d.ts +8 -0
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/notifications/telegram-daemon.d.ts +3 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +10 -10
- package/src/capability/mcp.ts +2 -0
- package/src/config/mcp-schema.json +5 -1
- package/src/config/settings-schema.ts +18 -2
- package/src/coordinator-mcp/server.ts +39 -4
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/skills.ts +30 -2
- package/src/gjc-runtime/session-state-sidecar.ts +167 -24
- package/src/gjc-runtime/team-runtime.ts +56 -51
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +1 -34
- package/src/modes/components/custom-editor.ts +18 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/model-selector.ts +58 -2
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +13 -0
- package/src/modes/components/status-line.ts +215 -86
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +171 -35
- package/src/modes/controllers/command-controller.ts +3 -2
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +158 -38
- package/src/modes/controllers/selector-controller.ts +25 -28
- package/src/modes/interactive-mode.ts +20 -23
- package/src/modes/prompt-action-autocomplete.ts +17 -7
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +9 -3
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/types.ts +1 -0
- package/src/notifications/index.ts +20 -1
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/telegram-daemon.ts +14 -0
- package/src/prompts/system/system-prompt.md +15 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +21 -1
- package/src/session/session-manager.ts +58 -18
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +15 -6
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/dist/types/discovery/vscode.d.ts +0 -1
- package/src/discovery/vscode.ts +0 -105
|
@@ -26,6 +26,7 @@ import { getPreset } from "./status-line/presets";
|
|
|
26
26
|
import { renderSegment, type SegmentContext } from "./status-line/segments";
|
|
27
27
|
import { getSeparator } from "./status-line/separators";
|
|
28
28
|
import { calculateTokensPerSecond } from "./status-line/token-rate";
|
|
29
|
+
import type { SeparatorDef } from "./status-line/types";
|
|
29
30
|
|
|
30
31
|
export interface StatusLineSegmentOptions {
|
|
31
32
|
model?: { showThinkingLevel?: boolean };
|
|
@@ -45,12 +46,30 @@ export interface StatusLineSettings {
|
|
|
45
46
|
showHookStatus?: boolean;
|
|
46
47
|
showSkillHud?: boolean;
|
|
47
48
|
sessionAccent?: boolean;
|
|
49
|
+
maxRows?: number;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface StatusLineComponentOptions {
|
|
51
53
|
version?: string;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
interface CollectedStatusSegments {
|
|
57
|
+
ctx: SegmentContext;
|
|
58
|
+
separatorDef: SeparatorDef;
|
|
59
|
+
bgAnsi: string;
|
|
60
|
+
fgAnsi: string;
|
|
61
|
+
sepAnsi: string;
|
|
62
|
+
left: string[];
|
|
63
|
+
leftSegIds: StatusLineSegmentId[];
|
|
64
|
+
right: string[];
|
|
65
|
+
previewHighlightSegment: StatusLineSegmentId | undefined;
|
|
66
|
+
sessionAccent: boolean | undefined;
|
|
67
|
+
leftSepWidth: number;
|
|
68
|
+
rightSepWidth: number;
|
|
69
|
+
leftCapWidth: number;
|
|
70
|
+
rightCapWidth: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
55
74
|
// Per-message token cache
|
|
56
75
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -215,6 +234,7 @@ export class StatusLineComponent implements Component {
|
|
|
215
234
|
showSkillHud: settings.get("statusLine.showSkillHud"),
|
|
216
235
|
segmentOptions: settings.getGroup("statusLine").segmentOptions,
|
|
217
236
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
237
|
+
maxRows: settings.get("statusLine.maxRows"),
|
|
218
238
|
};
|
|
219
239
|
this.#version = options.version?.trim() || undefined;
|
|
220
240
|
}
|
|
@@ -702,7 +722,70 @@ export class StatusLineComponent implements Component {
|
|
|
702
722
|
};
|
|
703
723
|
}
|
|
704
724
|
|
|
705
|
-
#
|
|
725
|
+
#groupWidth(parts: string[], capWidth: number, sepWidth: number): number {
|
|
726
|
+
if (parts.length === 0) return 0;
|
|
727
|
+
const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
|
|
728
|
+
const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
|
|
729
|
+
return partsWidth + sepTotal + 2 + capWidth;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#renderStatusGroup(
|
|
733
|
+
parts: string[],
|
|
734
|
+
direction: "left" | "right",
|
|
735
|
+
separatorDef: SeparatorDef,
|
|
736
|
+
bgAnsi: string,
|
|
737
|
+
fgAnsi: string,
|
|
738
|
+
sepAnsi: string,
|
|
739
|
+
): string {
|
|
740
|
+
if (parts.length === 0) return "";
|
|
741
|
+
const sep = direction === "left" ? separatorDef.left : separatorDef.right;
|
|
742
|
+
const cap = separatorDef.endCaps
|
|
743
|
+
? direction === "left"
|
|
744
|
+
? separatorDef.endCaps.right
|
|
745
|
+
: separatorDef.endCaps.left
|
|
746
|
+
: "";
|
|
747
|
+
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
|
|
748
|
+
const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
|
|
749
|
+
|
|
750
|
+
let content = bgAnsi + fgAnsi;
|
|
751
|
+
content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
|
|
752
|
+
content += "\x1b[0m";
|
|
753
|
+
|
|
754
|
+
if (capText) {
|
|
755
|
+
return direction === "right" ? capText + content : content + capText;
|
|
756
|
+
}
|
|
757
|
+
return content;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
#shrinkPathToWidth(content: string, ctx: SegmentContext, shrinkBy: number): string | null {
|
|
761
|
+
const currentPathVW = visibleWidth(content);
|
|
762
|
+
const minPathVW = 8; // icon + ellipsis + a few chars
|
|
763
|
+
const shrinkable = currentPathVW - minPathVW;
|
|
764
|
+
if (shrinkable <= 0 || shrinkBy <= 0) return null;
|
|
765
|
+
const targetShrink = Math.min(shrinkable, shrinkBy);
|
|
766
|
+
const currentMaxLen = ctx.options.path?.maxLength ?? 40;
|
|
767
|
+
let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - targetShrink);
|
|
768
|
+
const pathCtx = (maxLen: number): SegmentContext => ({
|
|
769
|
+
...ctx,
|
|
770
|
+
options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
|
|
771
|
+
});
|
|
772
|
+
let reRendered = renderSegment("path", pathCtx(newMaxLen));
|
|
773
|
+
if (!reRendered.visible || !reRendered.content) return null;
|
|
774
|
+
// maxLength governs path text, not icon prefix; iterate to compensate.
|
|
775
|
+
for (let i = 0; i < 8; i++) {
|
|
776
|
+
const saved = currentPathVW - visibleWidth(reRendered.content);
|
|
777
|
+
if (saved >= targetShrink) break;
|
|
778
|
+
const nextMaxLen = Math.max(4, newMaxLen - (targetShrink - saved));
|
|
779
|
+
if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
|
|
780
|
+
newMaxLen = nextMaxLen;
|
|
781
|
+
const adjusted = renderSegment("path", pathCtx(newMaxLen));
|
|
782
|
+
if (!adjusted.visible || !adjusted.content) break;
|
|
783
|
+
reRendered = adjusted;
|
|
784
|
+
}
|
|
785
|
+
return reRendered.content;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
#collectStatusSegments(width: number): CollectedStatusSegments {
|
|
706
789
|
const ctx = this.#buildSegmentContext(width);
|
|
707
790
|
const effectiveSettings = this.#resolveSettings();
|
|
708
791
|
const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
|
|
@@ -715,25 +798,25 @@ export class StatusLineComponent implements Component {
|
|
|
715
798
|
const fgAnsi = theme.getFgAnsi("text");
|
|
716
799
|
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
717
800
|
|
|
718
|
-
|
|
801
|
+
const previewHighlightSegment = effectiveSettings.previewHighlightSegment;
|
|
719
802
|
const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
|
|
720
|
-
|
|
803
|
+
previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
|
|
721
804
|
|
|
722
|
-
const
|
|
805
|
+
const left: string[] = [];
|
|
723
806
|
const leftSegIds: StatusLineSegmentId[] = [];
|
|
724
807
|
for (const segId of effectiveSettings.leftSegments) {
|
|
725
808
|
const rendered = renderSegment(segId, ctx);
|
|
726
809
|
if (rendered.visible && rendered.content) {
|
|
727
|
-
|
|
810
|
+
left.push(highlightSegment(segId, rendered.content));
|
|
728
811
|
leftSegIds.push(segId);
|
|
729
812
|
}
|
|
730
813
|
}
|
|
731
814
|
|
|
732
|
-
const
|
|
815
|
+
const right: string[] = [];
|
|
733
816
|
for (const segId of effectiveSettings.rightSegments) {
|
|
734
817
|
const rendered = renderSegment(segId, ctx);
|
|
735
818
|
if (rendered.visible && rendered.content) {
|
|
736
|
-
|
|
819
|
+
right.push(highlightSegment(segId, rendered.content));
|
|
737
820
|
}
|
|
738
821
|
}
|
|
739
822
|
|
|
@@ -742,116 +825,152 @@ export class StatusLineComponent implements Component {
|
|
|
742
825
|
if (runningBackgroundJobs > 0) {
|
|
743
826
|
const icon = theme.icon.agents ? `${theme.icon.agents} ` : "";
|
|
744
827
|
const label = `${formatCount("job", runningBackgroundJobs)} running`;
|
|
745
|
-
|
|
828
|
+
right.push(theme.fg("statusLineSubagents", `${icon}${label}`));
|
|
746
829
|
}
|
|
747
830
|
if (this.#version) {
|
|
748
|
-
|
|
831
|
+
right.push(theme.fg("dim", `v${this.#version}`));
|
|
749
832
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
ctx,
|
|
836
|
+
separatorDef,
|
|
837
|
+
bgAnsi,
|
|
838
|
+
fgAnsi,
|
|
839
|
+
sepAnsi,
|
|
840
|
+
left,
|
|
841
|
+
leftSegIds,
|
|
842
|
+
right,
|
|
843
|
+
previewHighlightSegment,
|
|
844
|
+
sessionAccent: effectiveSettings.sessionAccent,
|
|
845
|
+
leftSepWidth: visibleWidth(separatorDef.left),
|
|
846
|
+
rightSepWidth: visibleWidth(separatorDef.right),
|
|
847
|
+
leftCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0,
|
|
848
|
+
rightCapWidth: separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0,
|
|
764
849
|
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#resolveMaxRows(): number {
|
|
853
|
+
const raw = this.#settings.maxRows ?? 1;
|
|
854
|
+
if (!Number.isFinite(raw)) return 1;
|
|
855
|
+
return Math.max(1, Math.min(3, Math.trunc(raw)));
|
|
856
|
+
}
|
|
765
857
|
|
|
766
|
-
|
|
767
|
-
|
|
858
|
+
#buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
|
|
859
|
+
const seg = precollected ?? this.#collectStatusSegments(width);
|
|
860
|
+
const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
|
|
861
|
+
const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
|
|
862
|
+
const left = [...seg.left];
|
|
863
|
+
const leftIds = [...seg.leftSegIds];
|
|
864
|
+
const right = [...seg.right];
|
|
865
|
+
const topFillWidth = Math.max(0, width);
|
|
866
|
+
|
|
867
|
+
let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
868
|
+
let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
768
869
|
const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
|
|
769
870
|
|
|
770
871
|
if (topFillWidth > 0) {
|
|
771
872
|
while (totalWidth() > topFillWidth && right.length > 0) {
|
|
772
873
|
right.pop();
|
|
773
|
-
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
874
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
774
875
|
}
|
|
775
876
|
// Shrink path before dropping left segments — path is the only elastic segment
|
|
776
|
-
const pathIdx =
|
|
877
|
+
const pathIdx = leftIds.indexOf("path");
|
|
777
878
|
if (pathIdx >= 0 && totalWidth() > topFillWidth) {
|
|
778
879
|
const overflow = totalWidth() - topFillWidth;
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const shrinkBy = Math.min(shrinkable, overflow);
|
|
784
|
-
const currentMaxLen = ctx.options.path?.maxLength ?? 40;
|
|
785
|
-
let newMaxLen = Math.max(4, Math.min(currentMaxLen, currentPathVW) - shrinkBy);
|
|
786
|
-
const pathCtx = (maxLen: number): SegmentContext => ({
|
|
787
|
-
...ctx,
|
|
788
|
-
options: { ...ctx.options, path: { ...ctx.options.path, maxLength: maxLen } },
|
|
789
|
-
});
|
|
790
|
-
let reRendered = renderSegment("path", pathCtx(newMaxLen));
|
|
791
|
-
if (reRendered.visible && reRendered.content) {
|
|
792
|
-
// maxLength governs path text, not icon prefix; iterate to compensate
|
|
793
|
-
for (let i = 0; i < 8; i++) {
|
|
794
|
-
const saved = currentPathVW - visibleWidth(reRendered.content);
|
|
795
|
-
if (saved >= shrinkBy) break;
|
|
796
|
-
const nextMaxLen = Math.max(4, newMaxLen - (shrinkBy - saved));
|
|
797
|
-
if (nextMaxLen >= newMaxLen) break; // no progress or hit floor
|
|
798
|
-
newMaxLen = nextMaxLen;
|
|
799
|
-
const adjusted = renderSegment("path", pathCtx(newMaxLen));
|
|
800
|
-
if (!adjusted.visible || !adjusted.content) break;
|
|
801
|
-
reRendered = adjusted;
|
|
802
|
-
}
|
|
803
|
-
left[pathIdx] = highlightSegment("path", reRendered.content);
|
|
804
|
-
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
805
|
-
}
|
|
880
|
+
const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, overflow);
|
|
881
|
+
if (shrunk !== null) {
|
|
882
|
+
left[pathIdx] = previewHighlightSegment === "path" ? `\x1b[7m${shrunk}\x1b[27m` : shrunk;
|
|
883
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
806
884
|
}
|
|
807
885
|
}
|
|
808
886
|
while (totalWidth() > topFillWidth && left.length > 0) {
|
|
809
887
|
left.pop();
|
|
810
|
-
|
|
811
|
-
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
888
|
+
leftIds.pop();
|
|
889
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
812
890
|
}
|
|
813
891
|
}
|
|
814
892
|
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
const sep = direction === "left" ? separatorDef.left : separatorDef.right;
|
|
818
|
-
const cap = separatorDef.endCaps
|
|
819
|
-
? direction === "left"
|
|
820
|
-
? separatorDef.endCaps.right
|
|
821
|
-
: separatorDef.endCaps.left
|
|
822
|
-
: "";
|
|
823
|
-
const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
|
|
824
|
-
const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
|
|
825
|
-
|
|
826
|
-
let content = bgAnsi + fgAnsi;
|
|
827
|
-
content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
|
|
828
|
-
content += "\x1b[0m";
|
|
829
|
-
|
|
830
|
-
if (capText) {
|
|
831
|
-
return direction === "right" ? capText + content : content + capText;
|
|
832
|
-
}
|
|
833
|
-
return content;
|
|
834
|
-
};
|
|
835
|
-
|
|
836
|
-
const leftGroup = renderGroup(left, "left");
|
|
837
|
-
const rightGroup = renderGroup(right, "right");
|
|
893
|
+
const leftGroup = this.#renderStatusGroup(left, "left", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
894
|
+
const rightGroup = this.#renderStatusGroup(right, "right", separatorDef, bgAnsi, fgAnsi, sepAnsi);
|
|
838
895
|
if (!leftGroup && !rightGroup) return "";
|
|
839
896
|
|
|
840
897
|
if (topFillWidth === 0 || left.length === 0 || right.length === 0) {
|
|
841
898
|
return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
|
|
842
899
|
}
|
|
843
900
|
|
|
844
|
-
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
845
|
-
rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
|
|
901
|
+
leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
|
|
902
|
+
rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
|
|
846
903
|
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
|
847
|
-
const sessionName =
|
|
848
|
-
effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
904
|
+
const sessionName = seg.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
|
849
905
|
const accentHex = sessionName ? getSessionAccentHex(sessionName) : undefined;
|
|
850
906
|
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
|
851
907
|
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
|
852
908
|
return leftGroup + gapFill + rightGroup;
|
|
853
909
|
}
|
|
854
910
|
|
|
911
|
+
/**
|
|
912
|
+
* Multi-row status line. When `maxRows > 1` and the single-line layout would
|
|
913
|
+
* overflow, segments wrap onto additional left-justified rows instead of
|
|
914
|
+
* being dropped. Falls back to the polished justified single row whenever
|
|
915
|
+
* everything fits on one line.
|
|
916
|
+
*/
|
|
917
|
+
#buildStatusRows(width: number, maxRows: number): string[] {
|
|
918
|
+
const seg = this.#collectStatusSegments(width);
|
|
919
|
+
if (seg.left.length === 0 && seg.right.length === 0) return [];
|
|
920
|
+
|
|
921
|
+
const topFillWidth = Math.max(1, width);
|
|
922
|
+
const leftWidth = this.#groupWidth(seg.left, seg.leftCapWidth, seg.leftSepWidth);
|
|
923
|
+
const rightWidth = this.#groupWidth(seg.right, seg.rightCapWidth, seg.rightSepWidth);
|
|
924
|
+
const gap = seg.left.length > 0 && seg.right.length > 0 ? 1 : 0;
|
|
925
|
+
const fitsSingleRow = leftWidth + rightWidth + gap <= topFillWidth;
|
|
926
|
+
|
|
927
|
+
if (maxRows <= 1 || fitsSingleRow) {
|
|
928
|
+
const single = this.#buildStatusLine(width, seg);
|
|
929
|
+
return single ? [single] : [];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Ordered sequence: left segments first (in order), then right segments.
|
|
933
|
+
const items: { content: string; isPath: boolean }[] = [
|
|
934
|
+
...seg.left.map((content, i) => ({ content, isPath: seg.leftSegIds[i] === "path" })),
|
|
935
|
+
...seg.right.map(content => ({ content, isPath: false })),
|
|
936
|
+
];
|
|
937
|
+
|
|
938
|
+
// Pre-shrink an over-wide path so it never blows out a row on its own.
|
|
939
|
+
for (const item of items) {
|
|
940
|
+
if (!item.isPath) continue;
|
|
941
|
+
const alone = this.#groupWidth([item.content], seg.leftCapWidth, seg.leftSepWidth);
|
|
942
|
+
if (alone > topFillWidth) {
|
|
943
|
+
const shrunk = this.#shrinkPathToWidth(item.content, seg.ctx, alone - topFillWidth);
|
|
944
|
+
if (shrunk !== null) item.content = shrunk;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Greedy pack into up to `maxRows` left-justified rows.
|
|
949
|
+
const rows: string[][] = [];
|
|
950
|
+
let current: string[] = [];
|
|
951
|
+
for (const item of items) {
|
|
952
|
+
if (current.length === 0) {
|
|
953
|
+
current.push(item.content);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const tentative = [...current, item.content];
|
|
957
|
+
if (this.#groupWidth(tentative, seg.leftCapWidth, seg.leftSepWidth) <= topFillWidth) {
|
|
958
|
+
current = tentative;
|
|
959
|
+
} else {
|
|
960
|
+
rows.push(current);
|
|
961
|
+
current = [item.content];
|
|
962
|
+
if (rows.length >= maxRows) break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (rows.length < maxRows && current.length > 0) {
|
|
966
|
+
rows.push(current);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return rows.map(row =>
|
|
970
|
+
this.#renderStatusGroup(row, "left", seg.separatorDef, seg.bgAnsi, seg.fgAnsi, seg.sepAnsi),
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
855
974
|
getTopBorder(width: number): { content: string; width: number } {
|
|
856
975
|
const content = this.#buildStatusLine(width);
|
|
857
976
|
return {
|
|
@@ -860,6 +979,16 @@ export class StatusLineComponent implements Component {
|
|
|
860
979
|
};
|
|
861
980
|
}
|
|
862
981
|
|
|
982
|
+
/**
|
|
983
|
+
* Multi-row-aware content for the settings preview: the wrapped rows joined
|
|
984
|
+
* with newlines so a single `Text` renders them stacked. Honors the current
|
|
985
|
+
* `maxRows`; identical to the single status line when `maxRows` is 1 or when
|
|
986
|
+
* everything fits on one row.
|
|
987
|
+
*/
|
|
988
|
+
getPreviewContent(width: number): string {
|
|
989
|
+
return this.#buildStatusRows(width, this.#resolveMaxRows()).join("\n");
|
|
990
|
+
}
|
|
991
|
+
|
|
863
992
|
render(width: number): string[] {
|
|
864
993
|
const lines: string[] = [];
|
|
865
994
|
this.#refreshSkillHudInBackground();
|
|
@@ -868,9 +997,9 @@ export class StatusLineComponent implements Component {
|
|
|
868
997
|
lines.push(skillHud);
|
|
869
998
|
}
|
|
870
999
|
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
lines.push(truncateToWidth(
|
|
1000
|
+
const statusRows = this.#buildStatusRows(width, this.#resolveMaxRows());
|
|
1001
|
+
for (const statusRow of statusRows) {
|
|
1002
|
+
if (statusRow) lines.push(truncateToWidth(statusRow, width));
|
|
874
1003
|
}
|
|
875
1004
|
|
|
876
1005
|
const showHooks = this.#settings.showHookStatus ?? true;
|
|
@@ -135,6 +135,14 @@ class TreeList implements Component {
|
|
|
135
135
|
return this.#filteredNodes.length - 1;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
#clampSelectedIndex(): void {
|
|
139
|
+
if (this.#filteredNodes.length === 0) {
|
|
140
|
+
this.#selectedIndex = 0;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, this.#filteredNodes.length - 1));
|
|
144
|
+
}
|
|
145
|
+
|
|
138
146
|
#flattenTree(roots: SessionTreeNode[]): FlatNode[] {
|
|
139
147
|
const result: FlatNode[] = [];
|
|
140
148
|
this.#toolCallMap.clear();
|
|
@@ -333,12 +341,11 @@ class TreeList implements Component {
|
|
|
333
341
|
return true;
|
|
334
342
|
});
|
|
335
343
|
|
|
336
|
-
// Try to preserve cursor on the same node, or
|
|
344
|
+
// Try to preserve cursor on the same node, or keep the index in range.
|
|
337
345
|
if (this.#lastSelectedId) {
|
|
338
346
|
this.#selectedIndex = this.#findNearestVisibleIndex(this.#lastSelectedId);
|
|
339
|
-
} else
|
|
340
|
-
|
|
341
|
-
this.#selectedIndex = Math.max(0, this.#filteredNodes.length - 1);
|
|
347
|
+
} else {
|
|
348
|
+
this.#clampSelectedIndex();
|
|
342
349
|
}
|
|
343
350
|
|
|
344
351
|
// Update lastSelectedId to the actual selection (may have changed due to parent walk)
|
|
@@ -710,16 +717,29 @@ class TreeList implements Component {
|
|
|
710
717
|
}
|
|
711
718
|
|
|
712
719
|
handleInput(keyData: string): void {
|
|
720
|
+
const moveSelection = (delta: number, wrap: boolean): void => {
|
|
721
|
+
if (this.#filteredNodes.length === 0) return;
|
|
722
|
+
if (wrap) {
|
|
723
|
+
const next = this.#selectedIndex + delta;
|
|
724
|
+
this.#selectedIndex =
|
|
725
|
+
next < 0 ? this.#filteredNodes.length - 1 : next >= this.#filteredNodes.length ? 0 : next;
|
|
726
|
+
} else {
|
|
727
|
+
this.#selectedIndex += delta;
|
|
728
|
+
this.#clampSelectedIndex();
|
|
729
|
+
}
|
|
730
|
+
this.#lastSelectedId = this.#filteredNodes[this.#selectedIndex]?.node.entry.id ?? this.#lastSelectedId;
|
|
731
|
+
};
|
|
732
|
+
|
|
713
733
|
if (matchesKey(keyData, "up")) {
|
|
714
|
-
|
|
734
|
+
moveSelection(-1, true);
|
|
715
735
|
} else if (matchesKey(keyData, "down")) {
|
|
716
|
-
|
|
736
|
+
moveSelection(1, true);
|
|
717
737
|
} else if (matchesKey(keyData, "left")) {
|
|
718
738
|
// Page up
|
|
719
|
-
|
|
739
|
+
moveSelection(-this.maxVisibleLines, false);
|
|
720
740
|
} else if (matchesKey(keyData, "right")) {
|
|
721
741
|
// Page down
|
|
722
|
-
|
|
742
|
+
moveSelection(this.maxVisibleLines, false);
|
|
723
743
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
724
744
|
const selected = this.#filteredNodes[this.#selectedIndex];
|
|
725
745
|
if (selected && this.onSelect) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Component, padding, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
2
|
+
import { theme } from "../theme/theme";
|
|
3
|
+
|
|
4
|
+
export class TwoColumnBody implements Component {
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly leftPane: Component,
|
|
7
|
+
private readonly rightPane: Component,
|
|
8
|
+
private readonly maxHeight: number,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
render(width: number): string[] {
|
|
12
|
+
const leftWidth = Math.floor(width * 0.5);
|
|
13
|
+
const separatorText = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
14
|
+
const separatorWidth = width - leftWidth > 3 ? visibleWidth(separatorText) : 0;
|
|
15
|
+
const separator = separatorWidth > 0 ? separatorText : "";
|
|
16
|
+
const rightWidth = Math.max(0, width - leftWidth - separatorWidth);
|
|
17
|
+
const leftLines = this.leftPane.render(leftWidth);
|
|
18
|
+
const rightLines = this.rightPane.render(rightWidth);
|
|
19
|
+
const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
20
|
+
const out: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < lineCount; i++) {
|
|
23
|
+
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
24
|
+
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
25
|
+
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
26
|
+
out.push(leftPadded + separator + right);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
invalidate(): void {
|
|
33
|
+
this.leftPane.invalidate?.();
|
|
34
|
+
this.rightPane.invalidate?.();
|
|
35
|
+
}
|
|
36
|
+
}
|