@gajae-code/coding-agent 0.7.7 → 0.7.9
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 +38 -1
- package/README.md +14 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +9 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +45 -0
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +14 -5
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +7 -3
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/tools/ask.d.ts +24 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/package.json +7 -7
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +5 -3
- package/src/cli/update-cli.ts +155 -13
- package/src/cli.ts +19 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +136 -6
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +48 -0
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +280 -36
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +321 -38
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/team-runtime.ts +87 -7
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/gjc-runtime/ultragoal-guard.ts +11 -5
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +31 -192
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/model-selector.ts +260 -12
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +506 -5
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +70 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +5 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +142 -11
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +6 -1
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon.ts +6 -21
- package/src/prompts/system/system-prompt.md +5 -0
- package/src/sdk.ts +10 -1
- package/src/session/agent-session.ts +84 -6
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/ask.ts +11 -1
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
|
@@ -32,6 +32,7 @@ export interface StatusLineSegmentOptions {
|
|
|
32
32
|
path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
|
|
33
33
|
git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
|
|
34
34
|
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
35
|
+
usage?: { mode?: "used" | "remaining" };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface StatusLineSettings {
|
|
@@ -40,6 +41,7 @@ export interface StatusLineSettings {
|
|
|
40
41
|
rightSegments?: StatusLineSegmentId[];
|
|
41
42
|
separator?: StatusLineSeparatorStyle;
|
|
42
43
|
segmentOptions?: StatusLineSegmentOptions;
|
|
44
|
+
previewHighlightSegment?: StatusLineSegmentId;
|
|
43
45
|
showHookStatus?: boolean;
|
|
44
46
|
showSkillHud?: boolean;
|
|
45
47
|
sessionAccent?: boolean;
|
|
@@ -181,11 +183,8 @@ export class StatusLineComponent implements Component {
|
|
|
181
183
|
#lastTokensPerSecond: number | null = null;
|
|
182
184
|
#lastTokensPerSecondTimestamp: number | null = null;
|
|
183
185
|
|
|
184
|
-
//
|
|
185
|
-
#cachedUsage:
|
|
186
|
-
fiveHour?: { percent: number; resetMinutes?: number };
|
|
187
|
-
sevenDay?: { percent: number; resetHours?: number };
|
|
188
|
-
} | null = null;
|
|
186
|
+
// Provider usage caching (5-min TTL, OAuth/sub only)
|
|
187
|
+
#cachedUsage: SegmentContext["usage"] = null;
|
|
189
188
|
#usageFetchedAt = 0;
|
|
190
189
|
#usageInFlight = false;
|
|
191
190
|
// Context breakdown — incremental cache. Replaces the previous 2-second
|
|
@@ -221,7 +220,7 @@ export class StatusLineComponent implements Component {
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
updateSettings(settings: StatusLineSettings): void {
|
|
224
|
-
this.#settings = { ...this.#settings, ...settings };
|
|
223
|
+
this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
|
|
225
224
|
}
|
|
226
225
|
|
|
227
226
|
setAutoCompactEnabled(enabled: boolean): void {
|
|
@@ -462,9 +461,9 @@ export class StatusLineComponent implements Component {
|
|
|
462
461
|
}
|
|
463
462
|
|
|
464
463
|
/**
|
|
465
|
-
* Background-refresh the
|
|
466
|
-
*
|
|
467
|
-
*
|
|
464
|
+
* Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
|
|
465
|
+
* success (cache lifetime) and error (backoff). Exposed (non-private) so
|
|
466
|
+
* unit tests can verify the backoff invariant.
|
|
468
467
|
*/
|
|
469
468
|
refreshUsageInBackground(): void {
|
|
470
469
|
const now = Date.now();
|
|
@@ -478,6 +477,9 @@ export class StatusLineComponent implements Component {
|
|
|
478
477
|
.then(reports => {
|
|
479
478
|
this.#cachedUsage = this.#normalizeUsageReports(reports);
|
|
480
479
|
this.#usageFetchedAt = Date.now();
|
|
480
|
+
if (this.#onBranchChange) {
|
|
481
|
+
this.#onBranchChange();
|
|
482
|
+
}
|
|
481
483
|
})
|
|
482
484
|
.catch(() => {
|
|
483
485
|
// Backoff on error: stamp the fetch time so the 5-min TTL guard
|
|
@@ -491,47 +493,77 @@ export class StatusLineComponent implements Component {
|
|
|
491
493
|
});
|
|
492
494
|
}
|
|
493
495
|
|
|
494
|
-
#normalizeUsageReports(reports: unknown): {
|
|
495
|
-
fiveHour?: { percent: number; resetMinutes?: number };
|
|
496
|
-
sevenDay?: { percent: number; resetHours?: number };
|
|
497
|
-
} | null {
|
|
496
|
+
#normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
|
|
498
497
|
if (!Array.isArray(reports)) return null;
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
|
|
499
|
+
const seen = new Set<string>();
|
|
501
500
|
const now = Date.now();
|
|
501
|
+
|
|
502
|
+
const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
|
|
503
|
+
if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
|
|
504
|
+
return fallback;
|
|
505
|
+
};
|
|
506
|
+
const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
|
|
507
|
+
|
|
508
|
+
const pushWindow = (
|
|
509
|
+
key: string,
|
|
510
|
+
label: string,
|
|
511
|
+
fraction: number,
|
|
512
|
+
resetsAt: number | undefined,
|
|
513
|
+
resetUnit: "m" | "h",
|
|
514
|
+
) => {
|
|
515
|
+
if (seen.has(key)) return;
|
|
516
|
+
seen.add(key);
|
|
517
|
+
windows.push({
|
|
518
|
+
label,
|
|
519
|
+
percent: fraction * 100,
|
|
520
|
+
resetValue:
|
|
521
|
+
typeof resetsAt === "number"
|
|
522
|
+
? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
|
|
523
|
+
: undefined,
|
|
524
|
+
resetUnit,
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
|
|
502
528
|
for (const report of reports) {
|
|
503
529
|
if (!report || typeof report !== "object") continue;
|
|
530
|
+
const provider = (report as { provider?: unknown }).provider;
|
|
531
|
+
const providerId = typeof provider === "string" ? provider : undefined;
|
|
504
532
|
const limits = (report as { limits?: unknown }).limits;
|
|
505
533
|
if (!Array.isArray(limits)) continue;
|
|
506
534
|
for (const limit of limits) {
|
|
507
535
|
if (!limit || typeof limit !== "object") continue;
|
|
508
536
|
const l = limit as {
|
|
509
|
-
|
|
510
|
-
|
|
537
|
+
id?: unknown;
|
|
538
|
+
scope?: { windowId?: string; tier?: string; modelId?: string };
|
|
539
|
+
window?: { id?: string; resetsAt?: number };
|
|
511
540
|
amount?: { usedFraction?: number };
|
|
512
541
|
};
|
|
513
542
|
const fraction = l.amount?.usedFraction;
|
|
514
543
|
if (typeof fraction !== "number") continue;
|
|
515
|
-
const
|
|
544
|
+
const id = typeof l.id === "string" ? l.id : "";
|
|
545
|
+
const windowId = l.scope?.windowId ?? l.window?.id;
|
|
516
546
|
const tier = l.scope?.tier;
|
|
547
|
+
const modelId = l.scope?.modelId;
|
|
517
548
|
const resetsAt = l.window?.resetsAt;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
549
|
+
|
|
550
|
+
if (providerId === "openai-codex") {
|
|
551
|
+
if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
|
|
552
|
+
const label = codexWindowLabel(windowId, "primary");
|
|
553
|
+
pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
|
|
554
|
+
} else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
|
|
555
|
+
const label = codexWindowLabel(windowId, "secondary");
|
|
556
|
+
pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
|
|
557
|
+
}
|
|
558
|
+
} else if (windowId === "5h" && !tier) {
|
|
559
|
+
pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
|
|
560
|
+
} else if (windowId === "7d" && !tier) {
|
|
561
|
+
pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
|
|
530
562
|
}
|
|
531
563
|
}
|
|
532
564
|
}
|
|
533
|
-
|
|
534
|
-
return {
|
|
565
|
+
|
|
566
|
+
return windows.length > 0 ? { windows } : null;
|
|
535
567
|
}
|
|
536
568
|
|
|
537
569
|
/**
|
|
@@ -684,12 +716,15 @@ export class StatusLineComponent implements Component {
|
|
|
684
716
|
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
685
717
|
|
|
686
718
|
// Collect visible segment contents
|
|
719
|
+
const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
|
|
720
|
+
effectiveSettings.previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
|
|
721
|
+
|
|
687
722
|
const leftParts: string[] = [];
|
|
688
723
|
const leftSegIds: StatusLineSegmentId[] = [];
|
|
689
724
|
for (const segId of effectiveSettings.leftSegments) {
|
|
690
725
|
const rendered = renderSegment(segId, ctx);
|
|
691
726
|
if (rendered.visible && rendered.content) {
|
|
692
|
-
leftParts.push(rendered.content);
|
|
727
|
+
leftParts.push(highlightSegment(segId, rendered.content));
|
|
693
728
|
leftSegIds.push(segId);
|
|
694
729
|
}
|
|
695
730
|
}
|
|
@@ -698,7 +733,7 @@ export class StatusLineComponent implements Component {
|
|
|
698
733
|
for (const segId of effectiveSettings.rightSegments) {
|
|
699
734
|
const rendered = renderSegment(segId, ctx);
|
|
700
735
|
if (rendered.visible && rendered.content) {
|
|
701
|
-
rightParts.push(rendered.content);
|
|
736
|
+
rightParts.push(highlightSegment(segId, rendered.content));
|
|
702
737
|
}
|
|
703
738
|
}
|
|
704
739
|
|
|
@@ -765,7 +800,7 @@ export class StatusLineComponent implements Component {
|
|
|
765
800
|
if (!adjusted.visible || !adjusted.content) break;
|
|
766
801
|
reRendered = adjusted;
|
|
767
802
|
}
|
|
768
|
-
left[pathIdx] = reRendered.content;
|
|
803
|
+
left[pathIdx] = highlightSegment("path", reRendered.content);
|
|
769
804
|
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
770
805
|
}
|
|
771
806
|
}
|
|
@@ -3,6 +3,7 @@ import { calculatePromptTokens } from "@gajae-code/agent-core/compaction/compact
|
|
|
3
3
|
import type { AssistantMessage, ImageContent } from "@gajae-code/ai";
|
|
4
4
|
import { parseRateLimitReason } from "@gajae-code/ai";
|
|
5
5
|
import { type Component, Loader, TERMINAL, Text } from "@gajae-code/tui";
|
|
6
|
+
import { logger } from "@gajae-code/utils";
|
|
6
7
|
import { settings } from "../../config/settings";
|
|
7
8
|
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
8
9
|
import {
|
|
@@ -15,16 +16,56 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
|
15
16
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
16
17
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
17
18
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
19
|
+
import { summaryFromMessage } from "../../notifications/helpers";
|
|
18
20
|
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
19
21
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
20
22
|
import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
|
21
23
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
22
24
|
import { interruptHint } from "../shared";
|
|
23
25
|
import { buildAbortDisplayMessage } from "../utils/abort-message";
|
|
26
|
+
import { ringTerminalBell } from "../utils/terminal-bell";
|
|
24
27
|
|
|
25
28
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
26
29
|
|
|
27
30
|
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
|
31
|
+
const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
|
|
32
|
+
|
|
33
|
+
interface CompletionNotifyPayload {
|
|
34
|
+
type: "agent-turn-complete";
|
|
35
|
+
title: string;
|
|
36
|
+
body: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
sessionName?: string;
|
|
40
|
+
lastAssistantMessage?: string;
|
|
41
|
+
stopReason?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function completionNotifyShellCommand(command: string): string[] {
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
return ["cmd.exe", "/d", "/s", "/c", command];
|
|
47
|
+
}
|
|
48
|
+
return ["/bin/sh", "-c", command];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cleanNotificationEnvValue(value: string | undefined, max = 4000): string {
|
|
52
|
+
if (!value) return "";
|
|
53
|
+
return value.replaceAll("\0", "").slice(0, max);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildCompletionNotifyEnv(payload: CompletionNotifyPayload): Record<string, string> {
|
|
57
|
+
return {
|
|
58
|
+
GJC_NOTIFICATION_TYPE: payload.type,
|
|
59
|
+
GJC_NOTIFICATION_TITLE: cleanNotificationEnvValue(payload.title, 500),
|
|
60
|
+
GJC_NOTIFICATION_BODY: cleanNotificationEnvValue(payload.body),
|
|
61
|
+
GJC_NOTIFICATION_CWD: cleanNotificationEnvValue(payload.cwd, 2000),
|
|
62
|
+
GJC_NOTIFICATION_SESSION_ID: cleanNotificationEnvValue(payload.sessionId, 500),
|
|
63
|
+
GJC_NOTIFICATION_SESSION_NAME: cleanNotificationEnvValue(payload.sessionName, 500),
|
|
64
|
+
GJC_NOTIFICATION_LAST_ASSISTANT_MESSAGE: cleanNotificationEnvValue(payload.lastAssistantMessage),
|
|
65
|
+
GJC_NOTIFICATION_STOP_REASON: cleanNotificationEnvValue(payload.stopReason, 100),
|
|
66
|
+
GJC_NOTIFICATION_JSON: cleanNotificationEnvValue(JSON.stringify(payload), 8000),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
28
69
|
|
|
29
70
|
function friendlyRetryReason(errorMessage: string | undefined): string {
|
|
30
71
|
if (!errorMessage) return "";
|
|
@@ -857,8 +898,46 @@ export class EventController {
|
|
|
857
898
|
return lastAssistant?.usage ? calculatePromptTokens(lastAssistant.usage) : 0;
|
|
858
899
|
}
|
|
859
900
|
|
|
901
|
+
#runCompletionNotifyCommand(payload: CompletionNotifyPayload): void {
|
|
902
|
+
const command = settings.getGlobal("completion.notifyCommand")?.trim();
|
|
903
|
+
if (!command) return;
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
const proc = Bun.spawn(completionNotifyShellCommand(command), {
|
|
907
|
+
cwd: payload.cwd,
|
|
908
|
+
env: {
|
|
909
|
+
...process.env,
|
|
910
|
+
...buildCompletionNotifyEnv(payload),
|
|
911
|
+
},
|
|
912
|
+
stdin: "ignore",
|
|
913
|
+
stdout: "ignore",
|
|
914
|
+
stderr: "ignore",
|
|
915
|
+
});
|
|
916
|
+
proc.unref();
|
|
917
|
+
const timer = setTimeout(() => {
|
|
918
|
+
try {
|
|
919
|
+
proc.kill();
|
|
920
|
+
} catch {}
|
|
921
|
+
}, COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS);
|
|
922
|
+
timer.unref?.();
|
|
923
|
+
void proc.exited
|
|
924
|
+
.then(exitCode => {
|
|
925
|
+
clearTimeout(timer);
|
|
926
|
+
if (exitCode !== 0) {
|
|
927
|
+
logger.warn("completion notify command exited non-zero", { exitCode });
|
|
928
|
+
}
|
|
929
|
+
})
|
|
930
|
+
.catch(error => {
|
|
931
|
+
clearTimeout(timer);
|
|
932
|
+
logger.warn("completion notify command failed", { error: String(error) });
|
|
933
|
+
});
|
|
934
|
+
} catch (error) {
|
|
935
|
+
logger.warn("completion notify command failed to start", { error: String(error) });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
860
939
|
sendCompletionNotification(): void {
|
|
861
|
-
|
|
940
|
+
const isBackgrounded = this.ctx.isBackgrounded !== false;
|
|
862
941
|
const notify = settings.get("completion.notify");
|
|
863
942
|
if (notify === "off") return;
|
|
864
943
|
|
|
@@ -869,9 +948,24 @@ export class EventController {
|
|
|
869
948
|
const last = this.ctx.session.getLastAssistantMessage?.();
|
|
870
949
|
if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
|
|
871
950
|
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
951
|
+
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
952
|
+
const title = sessionName ? `${sessionName}: Complete` : "Complete";
|
|
953
|
+
const summary = summaryFromMessage(last, 1000);
|
|
954
|
+
const body = summary ?? "Complete";
|
|
955
|
+
const sessionManager = this.ctx.sessionManager as { getCwd?: () => string; getSessionId?: () => string };
|
|
956
|
+
const payload: CompletionNotifyPayload = {
|
|
957
|
+
type: "agent-turn-complete",
|
|
958
|
+
title,
|
|
959
|
+
body,
|
|
960
|
+
cwd: sessionManager.getCwd?.() ?? settings.getCwd(),
|
|
961
|
+
sessionId: sessionManager.getSessionId?.(),
|
|
962
|
+
sessionName: sessionName || undefined,
|
|
963
|
+
lastAssistantMessage: summary,
|
|
964
|
+
stopReason: last?.stopReason,
|
|
965
|
+
};
|
|
966
|
+
ringTerminalBell("complete");
|
|
967
|
+
if (isBackgrounded) TERMINAL.sendNotification(title);
|
|
968
|
+
this.#runCompletionNotifyCommand(payload);
|
|
875
969
|
}
|
|
876
970
|
|
|
877
971
|
async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
|
|
@@ -23,6 +23,7 @@ import { HookSelectorComponent } from "../../modes/components/hook-selector";
|
|
|
23
23
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
24
24
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
25
25
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
26
|
+
import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
|
|
26
27
|
|
|
27
28
|
const MAX_WIDGET_LINES = 10;
|
|
28
29
|
const HOOK_SELECTOR_CHROME_ROWS = 7;
|
|
@@ -622,6 +623,8 @@ export class ExtensionUiController {
|
|
|
622
623
|
const scrollTitleRows =
|
|
623
624
|
requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
|
|
624
625
|
|
|
626
|
+
ringTerminalBell(classifyHookSelectorBellEvent(title));
|
|
627
|
+
|
|
625
628
|
this.ctx.hookSelector = new HookSelectorComponent(
|
|
626
629
|
title,
|
|
627
630
|
options,
|
|
@@ -712,6 +715,7 @@ export class ExtensionUiController {
|
|
|
712
715
|
title: string,
|
|
713
716
|
placeholder?: string,
|
|
714
717
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
718
|
+
inputOptions?: { readonly initialValue?: string },
|
|
715
719
|
): Promise<string | undefined> {
|
|
716
720
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
717
721
|
() => this.hideHookInput(),
|
|
@@ -729,6 +733,7 @@ export class ExtensionUiController {
|
|
|
729
733
|
finish(undefined);
|
|
730
734
|
},
|
|
731
735
|
{
|
|
736
|
+
initialValue: inputOptions?.initialValue,
|
|
732
737
|
timeout: dialogOptions?.timeout,
|
|
733
738
|
onTimeout: dialogOptions?.onTimeout,
|
|
734
739
|
tui: this.ctx.ui,
|
|
@@ -9,6 +9,7 @@ import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibil
|
|
|
9
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
10
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
11
11
|
import { theme } from "../../modes/theme/theme";
|
|
12
|
+
import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
|
|
12
13
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
13
14
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
14
15
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
@@ -858,6 +859,7 @@ export class InputController {
|
|
|
858
859
|
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
859
860
|
copyPrompt: () => this.handleCopyPrompt(),
|
|
860
861
|
pasteImage: () => void this.handleImagePaste(),
|
|
862
|
+
scrollTmuxToPreviousUserInput: () => this.scrollTmuxToPreviousUserInput(),
|
|
861
863
|
undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
|
|
862
864
|
moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
|
|
863
865
|
moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
|
|
@@ -952,6 +954,19 @@ export class InputController {
|
|
|
952
954
|
this.setToolsExpanded(!this.ctx.toolOutputExpanded);
|
|
953
955
|
}
|
|
954
956
|
|
|
957
|
+
scrollTmuxToPreviousUserInput(): void {
|
|
958
|
+
const result = scrollTmuxPaneToPreviousUserInput();
|
|
959
|
+
if (result.ok) return;
|
|
960
|
+
|
|
961
|
+
if (result.reason === "not_inside_tmux") {
|
|
962
|
+
this.ctx.showWarning("Previous-input scroll works only inside tmux.");
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const detail = result.error ? `: ${result.error}` : ".";
|
|
967
|
+
this.ctx.showWarning(`Failed to scroll tmux to previous user input${detail}`);
|
|
968
|
+
}
|
|
969
|
+
|
|
955
970
|
setToolsExpanded(expanded: boolean): void {
|
|
956
971
|
this.ctx.toolOutputExpanded = expanded;
|
|
957
972
|
for (const child of this.ctx.chatContainer.children) {
|
|
@@ -4,10 +4,17 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
|
|
|
4
4
|
import type { Component, OverlayHandle } from "@gajae-code/tui";
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
|
|
6
6
|
import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
activateModelProfile,
|
|
9
|
+
type MaterializeModelProfileForDeletionResult,
|
|
10
|
+
materializeActiveModelProfileAssignment,
|
|
11
|
+
materializeModelProfileForDeletion,
|
|
12
|
+
restoreMaterializedModelProfileForDeletion,
|
|
13
|
+
} from "../../config/model-profile-activation";
|
|
14
|
+
import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
9
15
|
import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
|
|
10
16
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
17
|
+
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
11
18
|
import { settings } from "../../config/settings";
|
|
12
19
|
import { DebugSelectorComponent } from "../../debug";
|
|
13
20
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -38,7 +45,11 @@ import {
|
|
|
38
45
|
CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING,
|
|
39
46
|
runExternalCredentialAutoImport,
|
|
40
47
|
} from "../../setup/credential-auto-import";
|
|
41
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
filterAutoImportOAuthCredentials,
|
|
50
|
+
formatDiscoverySummary,
|
|
51
|
+
type ImportableCredential,
|
|
52
|
+
} from "../../setup/credential-import";
|
|
42
53
|
import {
|
|
43
54
|
MODEL_ONBOARDING_API_PROVIDER_COMMAND,
|
|
44
55
|
MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
|
|
@@ -239,7 +250,7 @@ export class SelectorController {
|
|
|
239
250
|
this.ctx.ui.requestRender();
|
|
240
251
|
}
|
|
241
252
|
|
|
242
|
-
showCustomModelPresetWizard(): void {
|
|
253
|
+
showCustomModelPresetWizard(snapshot: ModelProfileConfig): void {
|
|
243
254
|
this.showSelector(done => {
|
|
244
255
|
let wizard: CustomModelPresetWizardComponent;
|
|
245
256
|
const submit = async (input: CustomModelPresetWizardSubmit): Promise<void> => {
|
|
@@ -247,7 +258,7 @@ export class SelectorController {
|
|
|
247
258
|
const profile = await this.ctx.session.modelRegistry.saveCustomModelProfile(input.name, input.profile);
|
|
248
259
|
await this.ctx.session.modelRegistry.refresh("offline");
|
|
249
260
|
await this.ctx.notifyConfigChanged?.();
|
|
250
|
-
this.ctx.showStatus(`Custom model preset created: ${profile
|
|
261
|
+
this.ctx.showStatus(`Custom model preset created: ${formatModelProfileDisplayLabel(profile)}`);
|
|
251
262
|
done();
|
|
252
263
|
this.ctx.ui.requestRender();
|
|
253
264
|
} catch (err) {
|
|
@@ -256,6 +267,7 @@ export class SelectorController {
|
|
|
256
267
|
}
|
|
257
268
|
};
|
|
258
269
|
wizard = new CustomModelPresetWizardComponent(
|
|
270
|
+
snapshot,
|
|
259
271
|
input => {
|
|
260
272
|
void submit(input);
|
|
261
273
|
},
|
|
@@ -269,6 +281,106 @@ export class SelectorController {
|
|
|
269
281
|
});
|
|
270
282
|
}
|
|
271
283
|
|
|
284
|
+
async #renameCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
|
|
285
|
+
const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
|
|
286
|
+
const currentName = profile ? formatModelProfileDisplayLabel(profile) : profileName;
|
|
287
|
+
const input = await this.ctx.showHookInput(`Rename custom model preset: ${currentName}`, undefined, undefined, {
|
|
288
|
+
initialValue: currentName,
|
|
289
|
+
});
|
|
290
|
+
if (input === undefined) {
|
|
291
|
+
this.ctx.showStatus("Preset rename cancelled.");
|
|
292
|
+
this.ctx.ui.requestRender();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const renamed = await this.ctx.session.modelRegistry.renameCustomModelProfile(profileName, input);
|
|
297
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
298
|
+
await this.ctx.notifyConfigChanged?.();
|
|
299
|
+
modelSelector.refreshPresetProfiles(renamed.name);
|
|
300
|
+
this.ctx.showStatus(`Custom model preset renamed: ${formatModelProfileDisplayLabel(renamed)}`);
|
|
301
|
+
this.ctx.ui.requestRender();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.ctx.showError(`Preset rename failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async #deleteCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
|
|
308
|
+
const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
|
|
309
|
+
const profileLabel = profile ? formatModelProfileDisplayLabel(profile) : profileName;
|
|
310
|
+
const confirmed = await this.ctx.showHookConfirm(
|
|
311
|
+
`Delete custom model preset: ${profileLabel}`,
|
|
312
|
+
"This removes the preset entry after preserving current role model settings when this preset is active/default.",
|
|
313
|
+
);
|
|
314
|
+
if (!confirmed) {
|
|
315
|
+
this.ctx.showStatus("Preset delete cancelled.");
|
|
316
|
+
this.ctx.ui.requestRender();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const activeProfile = this.ctx.session.getActiveModelProfile?.();
|
|
321
|
+
const defaultProfile = this.ctx.settings.get("modelProfile.default");
|
|
322
|
+
let snapshot: MaterializeModelProfileForDeletionResult | undefined;
|
|
323
|
+
let deletedProfile: ModelProfileConfig | undefined;
|
|
324
|
+
const refreshSelectorState = (refreshedProfileName?: string): void => {
|
|
325
|
+
modelSelector.refreshRoleAssignments({
|
|
326
|
+
currentModel: this.ctx.session.model,
|
|
327
|
+
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
328
|
+
activeModelProfile:
|
|
329
|
+
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
330
|
+
});
|
|
331
|
+
modelSelector.refreshPresetProfiles(refreshedProfileName);
|
|
332
|
+
};
|
|
333
|
+
try {
|
|
334
|
+
if (activeProfile === profileName || defaultProfile === profileName) {
|
|
335
|
+
snapshot = await materializeModelProfileForDeletion({
|
|
336
|
+
session: this.ctx.session,
|
|
337
|
+
modelRegistry: this.ctx.session.modelRegistry,
|
|
338
|
+
settings: this.ctx.settings,
|
|
339
|
+
profileName,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
deletedProfile = await this.ctx.session.modelRegistry.deleteCustomModelProfile(profileName);
|
|
343
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
344
|
+
await this.ctx.notifyConfigChanged?.();
|
|
345
|
+
refreshSelectorState();
|
|
346
|
+
this.ctx.showStatus(`Custom model preset deleted: ${profileLabel}`);
|
|
347
|
+
this.ctx.ui.requestRender();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
let presetRestoreError: unknown;
|
|
350
|
+
if (deletedProfile) {
|
|
351
|
+
try {
|
|
352
|
+
await this.ctx.session.modelRegistry.saveCustomModelProfile(profileName, deletedProfile);
|
|
353
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
354
|
+
} catch (restoreErr) {
|
|
355
|
+
presetRestoreError = restoreErr;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (snapshot) {
|
|
359
|
+
try {
|
|
360
|
+
await restoreMaterializedModelProfileForDeletion({
|
|
361
|
+
settings: this.ctx.settings,
|
|
362
|
+
session: this.ctx.session,
|
|
363
|
+
snapshot,
|
|
364
|
+
});
|
|
365
|
+
} catch (restoreErr) {
|
|
366
|
+
refreshSelectorState(deletedProfile ? profileName : undefined);
|
|
367
|
+
this.ctx.showError(
|
|
368
|
+
`Preset delete failed and settings rollback failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`,
|
|
369
|
+
);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (deletedProfile) refreshSelectorState(profileName);
|
|
374
|
+
if (presetRestoreError) {
|
|
375
|
+
this.ctx.showError(
|
|
376
|
+
`Preset delete failed and preset restore failed: ${presetRestoreError instanceof Error ? presetRestoreError.message : String(presetRestoreError)}`,
|
|
377
|
+
);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.ctx.showError(`Preset delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
272
384
|
showCustomProviderWizard(): void {
|
|
273
385
|
this.showSelector(done => {
|
|
274
386
|
let wizard: CustomProviderWizardComponent;
|
|
@@ -336,14 +448,16 @@ export class SelectorController {
|
|
|
336
448
|
separator: settings.get("statusLine.separator"),
|
|
337
449
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
338
450
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
451
|
+
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
339
452
|
...previewSettings,
|
|
340
453
|
});
|
|
341
454
|
this.ctx.updateEditorTopBorder();
|
|
342
455
|
this.ctx.ui.requestRender();
|
|
343
456
|
},
|
|
344
|
-
getStatusLinePreview: () => {
|
|
457
|
+
getStatusLinePreview: (width?: number) => {
|
|
345
458
|
// Return the rendered status line for inline preview
|
|
346
|
-
const availableWidth =
|
|
459
|
+
const availableWidth =
|
|
460
|
+
width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
|
|
347
461
|
return this.ctx.statusLine.getTopBorder(availableWidth).content;
|
|
348
462
|
},
|
|
349
463
|
onPluginsChanged: () => {
|
|
@@ -359,6 +473,7 @@ export class SelectorController {
|
|
|
359
473
|
separator: settings.get("statusLine.separator"),
|
|
360
474
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
361
475
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
476
|
+
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
362
477
|
});
|
|
363
478
|
this.ctx.updateEditorTopBorder();
|
|
364
479
|
this.ctx.ui.requestRender();
|
|
@@ -603,6 +718,9 @@ export class SelectorController {
|
|
|
603
718
|
case "statusLineShowHooks":
|
|
604
719
|
case "statusLine.showHookStatus":
|
|
605
720
|
case "statusLine.sessionAccent":
|
|
721
|
+
case "statusLine.leftSegments":
|
|
722
|
+
case "statusLine.rightSegments":
|
|
723
|
+
case "statusLine.segmentOptions":
|
|
606
724
|
case "statusLineSegments":
|
|
607
725
|
case "statusLineModelThinking":
|
|
608
726
|
case "statusLinePathAbbreviate":
|
|
@@ -682,10 +800,23 @@ export class SelectorController {
|
|
|
682
800
|
try {
|
|
683
801
|
if (selection.kind === "createProfile") {
|
|
684
802
|
done();
|
|
685
|
-
this.showCustomModelPresetWizard();
|
|
803
|
+
this.showCustomModelPresetWizard(selection.profile);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (selection.kind === "renameProfile") {
|
|
807
|
+
await this.#renameCustomModelPreset(selection.profileName, modelSelector);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (selection.kind === "deleteProfile") {
|
|
811
|
+
await this.#deleteCustomModelPreset(selection.profileName, modelSelector);
|
|
686
812
|
return;
|
|
687
813
|
}
|
|
688
814
|
if (selection.kind === "profile") {
|
|
815
|
+
const profileLabel = formatModelProfileDisplayLabel(
|
|
816
|
+
this.ctx.session.modelRegistry.getModelProfile(selection.profileName) ?? {
|
|
817
|
+
name: selection.profileName,
|
|
818
|
+
},
|
|
819
|
+
);
|
|
689
820
|
await activateModelProfile(
|
|
690
821
|
{
|
|
691
822
|
session: this.ctx.session,
|
|
@@ -699,8 +830,8 @@ export class SelectorController {
|
|
|
699
830
|
this.ctx.updateEditorBorderColor();
|
|
700
831
|
this.ctx.showStatus(
|
|
701
832
|
selection.setDefault
|
|
702
|
-
? `Default model profile: ${
|
|
703
|
-
: `Model profile: ${
|
|
833
|
+
? `Default model profile: ${profileLabel}`
|
|
834
|
+
: `Model profile: ${profileLabel}`,
|
|
704
835
|
);
|
|
705
836
|
done();
|
|
706
837
|
this.ctx.ui.requestRender();
|
|
@@ -1330,7 +1461,7 @@ export class SelectorController {
|
|
|
1330
1461
|
}
|
|
1331
1462
|
}
|
|
1332
1463
|
|
|
1333
|
-
let externalCredentialCandidates:
|
|
1464
|
+
let externalCredentialCandidates: ImportableCredential[] = [];
|
|
1334
1465
|
if (
|
|
1335
1466
|
mode === "login" &&
|
|
1336
1467
|
providerId === undefined &&
|
|
@@ -2684,8 +2684,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2684
2684
|
this.#extensionUiController.hideHookSelector();
|
|
2685
2685
|
}
|
|
2686
2686
|
|
|
2687
|
-
showHookInput(
|
|
2688
|
-
|
|
2687
|
+
showHookInput(
|
|
2688
|
+
title: string,
|
|
2689
|
+
placeholder?: string,
|
|
2690
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
2691
|
+
inputOptions?: { readonly initialValue?: string },
|
|
2692
|
+
): Promise<string | undefined> {
|
|
2693
|
+
return this.#extensionUiController.showHookInput(title, placeholder, dialogOptions, inputOptions);
|
|
2689
2694
|
}
|
|
2690
2695
|
|
|
2691
2696
|
hideHookInput(): void {
|