@gajae-code/coding-agent 0.7.6 → 0.7.8
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 +26 -1
- package/README.md +8 -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/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +8 -3
- package/dist/types/config/models-config-schema.d.ts +5 -5
- package/dist/types/config/settings-schema.d.ts +123 -9
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
- 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 +37 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -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 +4 -3
- 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/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +7 -7
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +0 -1
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/update-cli.ts +155 -13
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +18 -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/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +58 -7
- package/src/config/models-config-schema.ts +10 -1
- package/src/config/settings-schema.ts +127 -10
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/gjc-runtime/launch-tmux.ts +34 -8
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/state-renderer.ts +13 -0
- package/src/gjc-runtime/team-runtime.ts +21 -6
- 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 +167 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/model-selector.ts +182 -5
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +491 -5
- package/src/modes/components/status-line/segments.ts +7 -18
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +69 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +18 -4
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/telegram-daemon-cli.ts +112 -9
- package/src/notifications/telegram-daemon.ts +4 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +118 -5
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/index.ts +2 -8
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
|
@@ -40,6 +40,7 @@ export interface StatusLineSettings {
|
|
|
40
40
|
rightSegments?: StatusLineSegmentId[];
|
|
41
41
|
separator?: StatusLineSeparatorStyle;
|
|
42
42
|
segmentOptions?: StatusLineSegmentOptions;
|
|
43
|
+
previewHighlightSegment?: StatusLineSegmentId;
|
|
43
44
|
showHookStatus?: boolean;
|
|
44
45
|
showSkillHud?: boolean;
|
|
45
46
|
sessionAccent?: boolean;
|
|
@@ -181,11 +182,8 @@ export class StatusLineComponent implements Component {
|
|
|
181
182
|
#lastTokensPerSecond: number | null = null;
|
|
182
183
|
#lastTokensPerSecondTimestamp: number | null = null;
|
|
183
184
|
|
|
184
|
-
//
|
|
185
|
-
#cachedUsage:
|
|
186
|
-
fiveHour?: { percent: number; resetMinutes?: number };
|
|
187
|
-
sevenDay?: { percent: number; resetHours?: number };
|
|
188
|
-
} | null = null;
|
|
185
|
+
// Provider usage caching (5-min TTL, OAuth/sub only)
|
|
186
|
+
#cachedUsage: SegmentContext["usage"] = null;
|
|
189
187
|
#usageFetchedAt = 0;
|
|
190
188
|
#usageInFlight = false;
|
|
191
189
|
// Context breakdown — incremental cache. Replaces the previous 2-second
|
|
@@ -221,7 +219,7 @@ export class StatusLineComponent implements Component {
|
|
|
221
219
|
}
|
|
222
220
|
|
|
223
221
|
updateSettings(settings: StatusLineSettings): void {
|
|
224
|
-
this.#settings = { ...this.#settings, ...settings };
|
|
222
|
+
this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
|
|
225
223
|
}
|
|
226
224
|
|
|
227
225
|
setAutoCompactEnabled(enabled: boolean): void {
|
|
@@ -462,9 +460,9 @@ export class StatusLineComponent implements Component {
|
|
|
462
460
|
}
|
|
463
461
|
|
|
464
462
|
/**
|
|
465
|
-
* Background-refresh the
|
|
466
|
-
*
|
|
467
|
-
*
|
|
463
|
+
* Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
|
|
464
|
+
* success (cache lifetime) and error (backoff). Exposed (non-private) so
|
|
465
|
+
* unit tests can verify the backoff invariant.
|
|
468
466
|
*/
|
|
469
467
|
refreshUsageInBackground(): void {
|
|
470
468
|
const now = Date.now();
|
|
@@ -478,6 +476,9 @@ export class StatusLineComponent implements Component {
|
|
|
478
476
|
.then(reports => {
|
|
479
477
|
this.#cachedUsage = this.#normalizeUsageReports(reports);
|
|
480
478
|
this.#usageFetchedAt = Date.now();
|
|
479
|
+
if (this.#onBranchChange) {
|
|
480
|
+
this.#onBranchChange();
|
|
481
|
+
}
|
|
481
482
|
})
|
|
482
483
|
.catch(() => {
|
|
483
484
|
// Backoff on error: stamp the fetch time so the 5-min TTL guard
|
|
@@ -491,47 +492,77 @@ export class StatusLineComponent implements Component {
|
|
|
491
492
|
});
|
|
492
493
|
}
|
|
493
494
|
|
|
494
|
-
#normalizeUsageReports(reports: unknown): {
|
|
495
|
-
fiveHour?: { percent: number; resetMinutes?: number };
|
|
496
|
-
sevenDay?: { percent: number; resetHours?: number };
|
|
497
|
-
} | null {
|
|
495
|
+
#normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
|
|
498
496
|
if (!Array.isArray(reports)) return null;
|
|
499
|
-
|
|
500
|
-
|
|
497
|
+
const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
|
|
498
|
+
const seen = new Set<string>();
|
|
501
499
|
const now = Date.now();
|
|
500
|
+
|
|
501
|
+
const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
|
|
502
|
+
if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
|
|
503
|
+
return fallback;
|
|
504
|
+
};
|
|
505
|
+
const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
|
|
506
|
+
|
|
507
|
+
const pushWindow = (
|
|
508
|
+
key: string,
|
|
509
|
+
label: string,
|
|
510
|
+
fraction: number,
|
|
511
|
+
resetsAt: number | undefined,
|
|
512
|
+
resetUnit: "m" | "h",
|
|
513
|
+
) => {
|
|
514
|
+
if (seen.has(key)) return;
|
|
515
|
+
seen.add(key);
|
|
516
|
+
windows.push({
|
|
517
|
+
label,
|
|
518
|
+
percent: fraction * 100,
|
|
519
|
+
resetValue:
|
|
520
|
+
typeof resetsAt === "number"
|
|
521
|
+
? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
|
|
522
|
+
: undefined,
|
|
523
|
+
resetUnit,
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
|
|
502
527
|
for (const report of reports) {
|
|
503
528
|
if (!report || typeof report !== "object") continue;
|
|
529
|
+
const provider = (report as { provider?: unknown }).provider;
|
|
530
|
+
const providerId = typeof provider === "string" ? provider : undefined;
|
|
504
531
|
const limits = (report as { limits?: unknown }).limits;
|
|
505
532
|
if (!Array.isArray(limits)) continue;
|
|
506
533
|
for (const limit of limits) {
|
|
507
534
|
if (!limit || typeof limit !== "object") continue;
|
|
508
535
|
const l = limit as {
|
|
509
|
-
|
|
510
|
-
|
|
536
|
+
id?: unknown;
|
|
537
|
+
scope?: { windowId?: string; tier?: string; modelId?: string };
|
|
538
|
+
window?: { id?: string; resetsAt?: number };
|
|
511
539
|
amount?: { usedFraction?: number };
|
|
512
540
|
};
|
|
513
541
|
const fraction = l.amount?.usedFraction;
|
|
514
542
|
if (typeof fraction !== "number") continue;
|
|
515
|
-
const
|
|
543
|
+
const id = typeof l.id === "string" ? l.id : "";
|
|
544
|
+
const windowId = l.scope?.windowId ?? l.window?.id;
|
|
516
545
|
const tier = l.scope?.tier;
|
|
546
|
+
const modelId = l.scope?.modelId;
|
|
517
547
|
const resetsAt = l.window?.resetsAt;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
548
|
+
|
|
549
|
+
if (providerId === "openai-codex") {
|
|
550
|
+
if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
|
|
551
|
+
const label = codexWindowLabel(windowId, "primary");
|
|
552
|
+
pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
|
|
553
|
+
} else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
|
|
554
|
+
const label = codexWindowLabel(windowId, "secondary");
|
|
555
|
+
pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
|
|
556
|
+
}
|
|
557
|
+
} else if (windowId === "5h" && !tier) {
|
|
558
|
+
pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
|
|
559
|
+
} else if (windowId === "7d" && !tier) {
|
|
560
|
+
pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
|
|
530
561
|
}
|
|
531
562
|
}
|
|
532
563
|
}
|
|
533
|
-
|
|
534
|
-
return {
|
|
564
|
+
|
|
565
|
+
return windows.length > 0 ? { windows } : null;
|
|
535
566
|
}
|
|
536
567
|
|
|
537
568
|
/**
|
|
@@ -684,12 +715,15 @@ export class StatusLineComponent implements Component {
|
|
|
684
715
|
const sepAnsi = theme.getFgAnsi("statusLineSep");
|
|
685
716
|
|
|
686
717
|
// Collect visible segment contents
|
|
718
|
+
const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
|
|
719
|
+
effectiveSettings.previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
|
|
720
|
+
|
|
687
721
|
const leftParts: string[] = [];
|
|
688
722
|
const leftSegIds: StatusLineSegmentId[] = [];
|
|
689
723
|
for (const segId of effectiveSettings.leftSegments) {
|
|
690
724
|
const rendered = renderSegment(segId, ctx);
|
|
691
725
|
if (rendered.visible && rendered.content) {
|
|
692
|
-
leftParts.push(rendered.content);
|
|
726
|
+
leftParts.push(highlightSegment(segId, rendered.content));
|
|
693
727
|
leftSegIds.push(segId);
|
|
694
728
|
}
|
|
695
729
|
}
|
|
@@ -698,7 +732,7 @@ export class StatusLineComponent implements Component {
|
|
|
698
732
|
for (const segId of effectiveSettings.rightSegments) {
|
|
699
733
|
const rendered = renderSegment(segId, ctx);
|
|
700
734
|
if (rendered.visible && rendered.content) {
|
|
701
|
-
rightParts.push(rendered.content);
|
|
735
|
+
rightParts.push(highlightSegment(segId, rendered.content));
|
|
702
736
|
}
|
|
703
737
|
}
|
|
704
738
|
|
|
@@ -765,7 +799,7 @@ export class StatusLineComponent implements Component {
|
|
|
765
799
|
if (!adjusted.visible || !adjusted.content) break;
|
|
766
800
|
reRendered = adjusted;
|
|
767
801
|
}
|
|
768
|
-
left[pathIdx] = reRendered.content;
|
|
802
|
+
left[pathIdx] = highlightSegment("path", reRendered.content);
|
|
769
803
|
leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
|
|
770
804
|
}
|
|
771
805
|
}
|
|
@@ -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,
|
|
@@ -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) {
|
|
@@ -8,6 +8,7 @@ import { activateModelProfile, materializeActiveModelProfileAssignment } from ".
|
|
|
8
8
|
import { recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
9
9
|
import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
|
|
10
10
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
11
|
+
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
11
12
|
import { settings } from "../../config/settings";
|
|
12
13
|
import { DebugSelectorComponent } from "../../debug";
|
|
13
14
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
setPreferredImageProvider,
|
|
52
53
|
setPreferredSearchProvider,
|
|
53
54
|
setSearchFallbackProviders,
|
|
55
|
+
setSearchHardTimeoutMs,
|
|
54
56
|
} from "../../tools";
|
|
55
57
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
56
58
|
import { AgentDashboard } from "../components/agent-dashboard";
|
|
@@ -238,7 +240,7 @@ export class SelectorController {
|
|
|
238
240
|
this.ctx.ui.requestRender();
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
showCustomModelPresetWizard(): void {
|
|
243
|
+
showCustomModelPresetWizard(snapshot: ModelProfileConfig): void {
|
|
242
244
|
this.showSelector(done => {
|
|
243
245
|
let wizard: CustomModelPresetWizardComponent;
|
|
244
246
|
const submit = async (input: CustomModelPresetWizardSubmit): Promise<void> => {
|
|
@@ -255,6 +257,7 @@ export class SelectorController {
|
|
|
255
257
|
}
|
|
256
258
|
};
|
|
257
259
|
wizard = new CustomModelPresetWizardComponent(
|
|
260
|
+
snapshot,
|
|
258
261
|
input => {
|
|
259
262
|
void submit(input);
|
|
260
263
|
},
|
|
@@ -335,14 +338,16 @@ export class SelectorController {
|
|
|
335
338
|
separator: settings.get("statusLine.separator"),
|
|
336
339
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
337
340
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
341
|
+
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
338
342
|
...previewSettings,
|
|
339
343
|
});
|
|
340
344
|
this.ctx.updateEditorTopBorder();
|
|
341
345
|
this.ctx.ui.requestRender();
|
|
342
346
|
},
|
|
343
|
-
getStatusLinePreview: () => {
|
|
347
|
+
getStatusLinePreview: (width?: number) => {
|
|
344
348
|
// Return the rendered status line for inline preview
|
|
345
|
-
const availableWidth =
|
|
349
|
+
const availableWidth =
|
|
350
|
+
width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
|
|
346
351
|
return this.ctx.statusLine.getTopBorder(availableWidth).content;
|
|
347
352
|
},
|
|
348
353
|
onPluginsChanged: () => {
|
|
@@ -358,6 +363,7 @@ export class SelectorController {
|
|
|
358
363
|
separator: settings.get("statusLine.separator"),
|
|
359
364
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
360
365
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
366
|
+
segmentOptions: settings.get("statusLine.segmentOptions"),
|
|
361
367
|
});
|
|
362
368
|
this.ctx.updateEditorTopBorder();
|
|
363
369
|
this.ctx.ui.requestRender();
|
|
@@ -602,6 +608,9 @@ export class SelectorController {
|
|
|
602
608
|
case "statusLineShowHooks":
|
|
603
609
|
case "statusLine.showHookStatus":
|
|
604
610
|
case "statusLine.sessionAccent":
|
|
611
|
+
case "statusLine.leftSegments":
|
|
612
|
+
case "statusLine.rightSegments":
|
|
613
|
+
case "statusLine.segmentOptions":
|
|
605
614
|
case "statusLineSegments":
|
|
606
615
|
case "statusLineModelThinking":
|
|
607
616
|
case "statusLinePathAbbreviate":
|
|
@@ -641,6 +650,11 @@ export class SelectorController {
|
|
|
641
650
|
);
|
|
642
651
|
}
|
|
643
652
|
break;
|
|
653
|
+
case "web_search.timeout":
|
|
654
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
655
|
+
setSearchHardTimeoutMs(value * 1000);
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
644
658
|
case "providers.image":
|
|
645
659
|
if (
|
|
646
660
|
value === "auto" ||
|
|
@@ -676,7 +690,7 @@ export class SelectorController {
|
|
|
676
690
|
try {
|
|
677
691
|
if (selection.kind === "createProfile") {
|
|
678
692
|
done();
|
|
679
|
-
this.showCustomModelPresetWizard();
|
|
693
|
+
this.showCustomModelPresetWizard(selection.profile);
|
|
680
694
|
return;
|
|
681
695
|
}
|
|
682
696
|
if (selection.kind === "profile") {
|
|
@@ -29,6 +29,7 @@ interface PromptActionAutocompleteOptions {
|
|
|
29
29
|
copyCurrentLine: () => void;
|
|
30
30
|
copyPrompt: () => void;
|
|
31
31
|
pasteImage: () => void;
|
|
32
|
+
scrollTmuxToPreviousUserInput: () => void;
|
|
32
33
|
undo: (prefix: string) => void;
|
|
33
34
|
moveCursorToMessageEnd: () => void;
|
|
34
35
|
moveCursorToMessageStart: () => void;
|
|
@@ -378,6 +379,13 @@ export function createPromptActionAutocompleteProvider(
|
|
|
378
379
|
keywords: ["paste", "image", "clipboard", "screenshot", "attach", "vision"],
|
|
379
380
|
execute: options.pasteImage,
|
|
380
381
|
},
|
|
382
|
+
{
|
|
383
|
+
id: "tmux-previous-user-input",
|
|
384
|
+
label: "Scroll to previous user input",
|
|
385
|
+
description: "tmux copy-mode",
|
|
386
|
+
keywords: ["scroll", "tmux", "previous", "user", "input", "prompt", "history"],
|
|
387
|
+
execute: options.scrollTmuxToPreviousUserInput,
|
|
388
|
+
},
|
|
381
389
|
{
|
|
382
390
|
id: "undo",
|
|
383
391
|
label: "Undo",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolveGjcTmuxCommand } from "../gjc-runtime/tmux-common";
|
|
2
|
+
|
|
3
|
+
// tmux copy-mode pads lines to the pane width, so allow trailing spaces before
|
|
4
|
+
// the line end while keeping the match scoped to the standalone `user` label.
|
|
5
|
+
export const TMUX_PREVIOUS_USER_INPUT_SEARCH_PATTERN = "^ *user *$";
|
|
6
|
+
|
|
7
|
+
type TmuxScrollFailureReason =
|
|
8
|
+
| "not_inside_tmux"
|
|
9
|
+
| "tmux_command_failed"
|
|
10
|
+
| "copy_mode_failed"
|
|
11
|
+
| "history_bottom_failed"
|
|
12
|
+
| "search_failed";
|
|
13
|
+
|
|
14
|
+
export type TmuxScrollToPreviousUserInputResult =
|
|
15
|
+
| { ok: true }
|
|
16
|
+
| { ok: false; reason: TmuxScrollFailureReason; error?: string };
|
|
17
|
+
|
|
18
|
+
interface TmuxSpawnResult {
|
|
19
|
+
exitCode: number | null;
|
|
20
|
+
stderr?: { toString(): string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TmuxSpawnSync = (command: string, args: string[], env: NodeJS.ProcessEnv) => TmuxSpawnResult;
|
|
24
|
+
|
|
25
|
+
function defaultSpawnSync(command: string, args: string[], env: NodeJS.ProcessEnv): TmuxSpawnResult {
|
|
26
|
+
return Bun.spawnSync([command, ...args], {
|
|
27
|
+
env,
|
|
28
|
+
stdout: "ignore",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getErrorText(result: TmuxSpawnResult): string | undefined {
|
|
34
|
+
const text = result.stderr?.toString().trim();
|
|
35
|
+
return text ? text : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runTmuxStep(
|
|
39
|
+
command: string,
|
|
40
|
+
args: string[],
|
|
41
|
+
env: NodeJS.ProcessEnv,
|
|
42
|
+
spawnSync: TmuxSpawnSync,
|
|
43
|
+
reason: TmuxScrollFailureReason,
|
|
44
|
+
): TmuxScrollToPreviousUserInputResult {
|
|
45
|
+
try {
|
|
46
|
+
const result = spawnSync(command, args, env);
|
|
47
|
+
if (result.exitCode === 0) return { ok: true };
|
|
48
|
+
return { ok: false, reason, error: getErrorText(result) };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return { ok: false, reason, error: error instanceof Error ? error.message : String(error) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function scrollTmuxToPreviousUserInput(
|
|
55
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
56
|
+
spawnSync: TmuxSpawnSync = defaultSpawnSync,
|
|
57
|
+
): TmuxScrollToPreviousUserInputResult {
|
|
58
|
+
if (!env.TMUX) return { ok: false, reason: "not_inside_tmux" };
|
|
59
|
+
|
|
60
|
+
let tmuxCommand: string;
|
|
61
|
+
try {
|
|
62
|
+
tmuxCommand = resolveGjcTmuxCommand(env);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
reason: "tmux_command_failed",
|
|
67
|
+
error: error instanceof Error ? error.message : String(error),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const targetPane = env.TMUX_PANE?.trim();
|
|
72
|
+
const targetArgs = targetPane ? ["-t", targetPane] : [];
|
|
73
|
+
const copyModeResult = runTmuxStep(tmuxCommand, ["copy-mode", ...targetArgs], env, spawnSync, "copy_mode_failed");
|
|
74
|
+
if (!copyModeResult.ok) return copyModeResult;
|
|
75
|
+
|
|
76
|
+
const historyBottomResult = runTmuxStep(
|
|
77
|
+
tmuxCommand,
|
|
78
|
+
["send-keys", ...targetArgs, "-X", "history-bottom"],
|
|
79
|
+
env,
|
|
80
|
+
spawnSync,
|
|
81
|
+
"history_bottom_failed",
|
|
82
|
+
);
|
|
83
|
+
if (!historyBottomResult.ok) return historyBottomResult;
|
|
84
|
+
|
|
85
|
+
return runTmuxStep(
|
|
86
|
+
tmuxCommand,
|
|
87
|
+
["send-keys", ...targetArgs, "-X", "search-backward", TMUX_PREVIOUS_USER_INPUT_SEARCH_PATTERN],
|
|
88
|
+
env,
|
|
89
|
+
spawnSync,
|
|
90
|
+
"search_failed",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { settings } from "../../config/settings";
|
|
2
|
+
|
|
3
|
+
export type TerminalBellEvent = "complete" | "approval" | "ask";
|
|
4
|
+
|
|
5
|
+
const BEL = "\x07";
|
|
6
|
+
|
|
7
|
+
function getBellSetting(key: Parameters<typeof settings.get>[0]): boolean {
|
|
8
|
+
try {
|
|
9
|
+
return Boolean(settings.get(key));
|
|
10
|
+
} catch {
|
|
11
|
+
// Some component-level tests exercise UI helpers before Settings.init().
|
|
12
|
+
// Terminal bells are purely best-effort, so an unavailable settings store
|
|
13
|
+
// must not make the UI path throw.
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function enabledForEvent(event: TerminalBellEvent): boolean {
|
|
19
|
+
if (!getBellSetting("notifications.terminalBell")) return false;
|
|
20
|
+
switch (event) {
|
|
21
|
+
case "complete":
|
|
22
|
+
return getBellSetting("notifications.bellOnComplete");
|
|
23
|
+
case "approval":
|
|
24
|
+
return getBellSetting("notifications.bellOnApproval");
|
|
25
|
+
case "ask":
|
|
26
|
+
return getBellSetting("notifications.bellOnAsk");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ringTerminalBell(
|
|
31
|
+
event: TerminalBellEvent,
|
|
32
|
+
output: Pick<NodeJS.WriteStream, "write"> = process.stdout,
|
|
33
|
+
): void {
|
|
34
|
+
if (!enabledForEvent(event)) return;
|
|
35
|
+
try {
|
|
36
|
+
output.write(BEL);
|
|
37
|
+
} catch {
|
|
38
|
+
// Best-effort local notification only.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function classifyHookSelectorBellEvent(title: string): TerminalBellEvent {
|
|
43
|
+
const normalized = title.toLowerCase();
|
|
44
|
+
if (normalized.includes("approval") || normalized.includes("approve") || normalized.includes("plan ready")) {
|
|
45
|
+
return "approval";
|
|
46
|
+
}
|
|
47
|
+
return "ask";
|
|
48
|
+
}
|