@gajae-code/coding-agent 0.11.7 → 0.11.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 +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +41 -27
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +198 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -145,6 +145,8 @@ async function assertDirectoryNotSymlink(directoryPath: string): Promise<void> {
|
|
|
145
145
|
const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
|
|
146
146
|
const MAX_LEGACY_LOCAL_BYTES = 64 * 1024 * 1024;
|
|
147
147
|
|
|
148
|
+
type LegacyMigrationState = "complete" | "cleanup_pending";
|
|
149
|
+
|
|
148
150
|
interface LegacyEntrySnapshot {
|
|
149
151
|
readonly relativePath: string;
|
|
150
152
|
readonly dev: bigint;
|
|
@@ -334,7 +336,7 @@ async function migrateManagedLegacyLocal(
|
|
|
334
336
|
}
|
|
335
337
|
const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
|
|
336
338
|
const installed: Array<{ readonly path: string; readonly dev: bigint; readonly ino: bigint }> = [];
|
|
337
|
-
let
|
|
339
|
+
let sourceRetired = false;
|
|
338
340
|
try {
|
|
339
341
|
await fs.mkdir(staging, { mode: 0o700 });
|
|
340
342
|
for (const entry of captured.entries) {
|
|
@@ -360,18 +362,22 @@ async function migrateManagedLegacyLocal(
|
|
|
360
362
|
const identity = await fs.lstat(destination, { bigint: true });
|
|
361
363
|
installed.push({ path: destination, dev: identity.dev, ino: identity.ino });
|
|
362
364
|
}
|
|
365
|
+
try {
|
|
366
|
+
source.retire(captured.snapshot);
|
|
367
|
+
sourceRetired = true;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (!(error instanceof Error) || error.message !== "cleanup_pending") throw error;
|
|
370
|
+
await fs.writeFile(marker, "cleanup_pending\n", { mode: 0o600, flag: "wx" });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
363
373
|
await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
|
|
364
|
-
const markerIdentity = await fs.lstat(marker, { bigint: true });
|
|
365
|
-
installedMarker = { dev: markerIdentity.dev, ino: markerIdentity.ino };
|
|
366
|
-
source.retire(captured.snapshot);
|
|
367
374
|
} catch (error) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
await fs.rm(destination.path, { recursive: true, force: true });
|
|
375
|
+
if (!sourceRetired) {
|
|
376
|
+
for (const destination of installed.reverse()) {
|
|
377
|
+
const current = await fs.lstat(destination.path, { bigint: true }).catch(() => null);
|
|
378
|
+
if (current?.dev === destination.dev && current.ino === destination.ino)
|
|
379
|
+
await fs.rm(destination.path, { recursive: true, force: true });
|
|
380
|
+
}
|
|
375
381
|
}
|
|
376
382
|
throw error;
|
|
377
383
|
} finally {
|
|
@@ -379,15 +385,16 @@ async function migrateManagedLegacyLocal(
|
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
387
|
|
|
382
|
-
async function readMigrationMarker(marker: string): Promise<
|
|
388
|
+
async function readMigrationMarker(marker: string): Promise<LegacyMigrationState | null> {
|
|
383
389
|
try {
|
|
384
390
|
const snapshot = await snapshotRegularFile(marker, LEGACY_MIGRATION_MARKER);
|
|
385
391
|
if (snapshot.size > 32n || snapshot.bytes === undefined) throw new Error("Unsafe local:// migration marker");
|
|
386
392
|
const value = snapshot.bytes.toString("utf8");
|
|
387
|
-
if (value
|
|
388
|
-
return
|
|
393
|
+
if (value === "cleanup_pending\n") return "cleanup_pending";
|
|
394
|
+
if (value === "verified\n" || value === "absent\n") return "complete";
|
|
395
|
+
throw new Error("Unsafe local:// migration marker");
|
|
389
396
|
} catch (error) {
|
|
390
|
-
if (isEnoent(error)) return
|
|
397
|
+
if (isEnoent(error)) return null;
|
|
391
398
|
throw error;
|
|
392
399
|
}
|
|
393
400
|
}
|
|
@@ -436,7 +443,17 @@ async function migrateLegacyLocal(
|
|
|
436
443
|
}
|
|
437
444
|
await fs.rename(path.join(staging, entry), path.join(localRoot, entry));
|
|
438
445
|
}
|
|
439
|
-
|
|
446
|
+
try {
|
|
447
|
+
await retireLegacyTree(legacySource.root, manifest);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (
|
|
450
|
+
!(error instanceof Error) ||
|
|
451
|
+
error.message !== "Legacy local:// migration retirement failed: cleanup_pending"
|
|
452
|
+
)
|
|
453
|
+
throw error;
|
|
454
|
+
await fs.writeFile(marker, "cleanup_pending\n", { mode: 0o600, flag: "wx" });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
440
457
|
await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
|
|
441
458
|
} finally {
|
|
442
459
|
await fs.rm(staging, { recursive: true, force: true });
|
|
@@ -2,29 +2,91 @@ import { MCPManager } from "../runtime-mcp/manager";
|
|
|
2
2
|
import type { MCPResourceReadResult } from "../runtime-mcp/types";
|
|
3
3
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const MAX_MCP_RESOURCE_URI_CHARS = 16_384;
|
|
6
|
+
const MAX_MCP_URI_TEMPLATE_CHARS = 8_192;
|
|
7
|
+
const MAX_MCP_URI_TEMPLATE_EXPRESSIONS = 32;
|
|
8
|
+
|
|
9
|
+
interface CompiledUriTemplate {
|
|
10
|
+
literalSegments: string[];
|
|
11
|
+
literalChars: number;
|
|
12
|
+
expressionCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function containsExpressionLineTerminator(text: string, start: number, end: number): boolean {
|
|
16
|
+
for (let index = start; index < end; index++) {
|
|
17
|
+
const code = text.charCodeAt(index);
|
|
18
|
+
if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compileUriTemplate(uriTemplate: string): CompiledUriTemplate | undefined {
|
|
24
|
+
if (uriTemplate.length > MAX_MCP_URI_TEMPLATE_CHARS) return undefined;
|
|
25
|
+
|
|
26
|
+
const literalSegments: string[] = [];
|
|
27
|
+
let expressionCount = 0;
|
|
28
|
+
let literalChars = 0;
|
|
29
|
+
let segmentStart = 0;
|
|
30
|
+
let scanPosition = 0;
|
|
31
|
+
while (scanPosition < uriTemplate.length) {
|
|
32
|
+
const expressionStart = uriTemplate.indexOf("{", scanPosition);
|
|
33
|
+
if (expressionStart < 0) break;
|
|
34
|
+
const expressionEnd = uriTemplate.indexOf("}", expressionStart + 1);
|
|
35
|
+
if (expressionEnd < 0) break;
|
|
36
|
+
if (expressionEnd === expressionStart + 1) {
|
|
37
|
+
scanPosition = expressionStart + 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
expressionCount++;
|
|
41
|
+
if (expressionCount > MAX_MCP_URI_TEMPLATE_EXPRESSIONS) return undefined;
|
|
42
|
+
const segment = uriTemplate.slice(segmentStart, expressionStart);
|
|
43
|
+
literalSegments.push(segment);
|
|
44
|
+
literalChars += segment.length;
|
|
45
|
+
segmentStart = expressionEnd + 1;
|
|
46
|
+
scanPosition = segmentStart;
|
|
47
|
+
}
|
|
48
|
+
const finalSegment = uriTemplate.slice(segmentStart);
|
|
49
|
+
literalSegments.push(finalSegment);
|
|
50
|
+
literalChars += finalSegment.length;
|
|
51
|
+
return { literalSegments, literalChars, expressionCount };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function matchesUriTemplate(uri: string, template: CompiledUriTemplate): boolean {
|
|
55
|
+
const { literalSegments, expressionCount } = template;
|
|
56
|
+
if (expressionCount === 0) return uri === literalSegments[0];
|
|
57
|
+
if (!uri.startsWith(literalSegments[0])) return false;
|
|
58
|
+
|
|
59
|
+
let cursor = literalSegments[0].length;
|
|
60
|
+
for (let index = 1; index < literalSegments.length - 1; index++) {
|
|
61
|
+
const segment = literalSegments[index];
|
|
62
|
+
const matchIndex = uri.indexOf(segment, cursor);
|
|
63
|
+
if (matchIndex < 0 || containsExpressionLineTerminator(uri, cursor, matchIndex)) return false;
|
|
64
|
+
cursor = matchIndex + segment.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const suffix = literalSegments.at(-1) ?? "";
|
|
68
|
+
const suffixStart = uri.length - suffix.length;
|
|
69
|
+
return suffixStart >= cursor && uri.endsWith(suffix) && !containsExpressionLineTerminator(uri, cursor, suffixStart);
|
|
7
70
|
}
|
|
8
71
|
|
|
9
72
|
function getUriTemplateMatchScore(
|
|
10
73
|
uri: string,
|
|
11
74
|
uriTemplate: string,
|
|
12
75
|
): { literalChars: number; expressionCount: number } | undefined {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const pattern = literalSegments.map(escapeRegex).join("(.*?)");
|
|
17
|
-
const regex = new RegExp(`^${pattern}$`);
|
|
18
|
-
if (!regex.test(uri)) return undefined;
|
|
19
|
-
const literalChars = literalSegments.reduce((total, segment) => total + segment.length, 0);
|
|
20
|
-
return { literalChars, expressionCount };
|
|
76
|
+
const template = compileUriTemplate(uriTemplate);
|
|
77
|
+
if (!template || !matchesUriTemplate(uri, template)) return undefined;
|
|
78
|
+
return { literalChars: template.literalChars, expressionCount: template.expressionCount };
|
|
21
79
|
}
|
|
22
80
|
|
|
23
81
|
function extractResourceUri(url: InternalUrl): string {
|
|
24
82
|
const host = url.rawHost || url.hostname;
|
|
25
83
|
const rawPathname = url.rawPathname ?? url.pathname;
|
|
26
84
|
const hasPath = rawPathname && rawPathname !== "/";
|
|
27
|
-
const
|
|
85
|
+
const rawUri = `${host}${hasPath ? rawPathname : ""}${url.search}${url.hash}`;
|
|
86
|
+
if (rawUri.length > MAX_MCP_RESOURCE_URI_CHARS) {
|
|
87
|
+
throw new Error("MCP resource URI exceeds the 16384-character limit.");
|
|
88
|
+
}
|
|
89
|
+
const uri = rawUri.trim();
|
|
28
90
|
if (!uri) {
|
|
29
91
|
throw new Error("mcp:// URL requires a resource URI: mcp://<resource-uri>");
|
|
30
92
|
}
|
package/src/main.ts
CHANGED
|
@@ -11,7 +11,6 @@ import * as path from "node:path";
|
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
12
|
import type { ImageContent } from "@gajae-code/ai";
|
|
13
13
|
import {
|
|
14
|
-
$env,
|
|
15
14
|
$pickenv,
|
|
16
15
|
getAgentDir,
|
|
17
16
|
getProjectDir,
|
|
@@ -1119,6 +1118,27 @@ export function resolveModelRoleOverrides(parsed: Pick<Args, "smol" | "slow" | "
|
|
|
1119
1118
|
if (plan) overrides.plan = plan;
|
|
1120
1119
|
return overrides;
|
|
1121
1120
|
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Apply the `--no-pty` / `--no-title` terminal-control flags to the environment.
|
|
1124
|
+
*
|
|
1125
|
+
* Sets the canonical `GJC_*` name (so an explicit flag wins over a user-set
|
|
1126
|
+
* `GJC_*` value under the GJC-first resolver — CLI authority) and the legacy
|
|
1127
|
+
* `PI_*` name for backward compatibility. `--acp` mode implies `--no-title`.
|
|
1128
|
+
*/
|
|
1129
|
+
export function applyTerminalControlFlagsToEnv(
|
|
1130
|
+
parsed: Pick<Args, "noPty" | "noTitle" | "mode">,
|
|
1131
|
+
env: NodeJS.ProcessEnv = Bun.env,
|
|
1132
|
+
): void {
|
|
1133
|
+
if (parsed.noPty) {
|
|
1134
|
+
env.GJC_NO_PTY = "1";
|
|
1135
|
+
env.PI_NO_PTY = "1";
|
|
1136
|
+
}
|
|
1137
|
+
if (parsed.noTitle || parsed.mode === "acp") {
|
|
1138
|
+
env.GJC_NO_TITLE = "1";
|
|
1139
|
+
env.PI_NO_TITLE = "1";
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1122
1142
|
export async function runRootCommand(
|
|
1123
1143
|
parsed: Args,
|
|
1124
1144
|
rawArgs: string[],
|
|
@@ -1258,12 +1278,7 @@ export async function runRootCommand(
|
|
|
1258
1278
|
applyAcpDefaultSettingOverrides(settingsInstance);
|
|
1259
1279
|
}
|
|
1260
1280
|
modelRegistry.applyConfiguredModelBindings(settingsInstance);
|
|
1261
|
-
|
|
1262
|
-
Bun.env.PI_NO_PTY = "1";
|
|
1263
|
-
}
|
|
1264
|
-
if (parsedArgs.noTitle || parsedArgs.mode === "acp") {
|
|
1265
|
-
Bun.env.PI_NO_TITLE = "1";
|
|
1266
|
-
}
|
|
1281
|
+
applyTerminalControlFlagsToEnv(parsedArgs);
|
|
1267
1282
|
const hasPreparedInput = parsedArgs.messages.length > 0 || parsedArgs.fileArgs.length > 0;
|
|
1268
1283
|
const { pipedInput, fileText, fileImages } = await logger.time("prepareInitialMessage", async () => {
|
|
1269
1284
|
const pipedInput =
|
|
@@ -1613,9 +1628,9 @@ export async function runRootCommand(
|
|
|
1613
1628
|
process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+N to cycle)")}`)}\n`);
|
|
1614
1629
|
}
|
|
1615
1630
|
|
|
1616
|
-
if ($
|
|
1631
|
+
if ($pickenv("GJC_TIMING", "PI_TIMING")) {
|
|
1617
1632
|
logger.printTimings();
|
|
1618
|
-
exitForTiming = $
|
|
1633
|
+
exitForTiming = $pickenv("GJC_TIMING", "PI_TIMING") === "x";
|
|
1619
1634
|
}
|
|
1620
1635
|
|
|
1621
1636
|
if (!exitForTiming) {
|
|
@@ -1660,7 +1675,7 @@ export async function runRootCommand(
|
|
|
1660
1675
|
initialImages,
|
|
1661
1676
|
suppressProcessExit: deps.suppressProcessExit,
|
|
1662
1677
|
});
|
|
1663
|
-
if ($
|
|
1678
|
+
if ($pickenv("GJC_TIMING", "PI_TIMING")) {
|
|
1664
1679
|
logger.printTimings();
|
|
1665
1680
|
}
|
|
1666
1681
|
} finally {
|
package/src/modes/DESIGN.md
CHANGED
|
@@ -94,6 +94,11 @@ empty space and list density for an operational setup flow.
|
|
|
94
94
|
removing line breaks. Notification secrets will use the dedicated masked
|
|
95
95
|
input from Work item 6; they must never appear in list values, descriptions,
|
|
96
96
|
previews, artifacts, or logs.
|
|
97
|
+
### Shortcut labels and binding authority
|
|
98
|
+
|
|
99
|
+
Keybinding configuration is a portable canonical grammar: textual key IDs use `ctrl`, `alt`, `shift`, and `super` plus a key name (for example, `ctrl+p` or `alt+enter`). Do not serialize or require display-only labels. Runtime UI renders those IDs through the shared formatter for its explicit platform context; macOS uses MacBook-style glyphs (`⌃`, `⌥`, `⇧`, `⌘`, `↩`, `⎋`, `⇥`, `⌫`, `⌦`, and arrow glyphs) while other platforms use textual labels. A glyph is never configuration syntax.
|
|
100
|
+
|
|
101
|
+
Static onboarding and generated documentation have authority only over shipped defaults. Keep generated tables host-independent by showing canonical textual IDs, not the capture host's labels. The runtime `KeybindingsManager` owns the effective binding set after user remaps and extensions load; `/hotkeys` and runtime hints must render that effective set with the platform context injected by their host. Do not let a static onboarding hint imply that it reflects remaps.
|
|
97
102
|
|
|
98
103
|
### Status, errors, confirmation, and disabled work
|
|
99
104
|
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
AcpSdkAdapterError,
|
|
43
43
|
} from "../../sdk/acp";
|
|
44
44
|
import { ensureBroker } from "../../sdk/broker/ensure";
|
|
45
|
-
import { readSdkBrokerDiscovery, SdkClient } from "../../sdk/client";
|
|
45
|
+
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
|
|
46
46
|
import { mapAgentWireEventPayloadToAcpSessionUpdates } from "./acp-event-mapper";
|
|
47
47
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
48
48
|
import type { AcpStartupOptions } from "./startup-options";
|
|
@@ -88,6 +88,7 @@ type PendingAttachment = { epoch: number; task: Promise<void> };
|
|
|
88
88
|
type SessionRecord = {
|
|
89
89
|
cwd: string;
|
|
90
90
|
adapter: AcpSdkAdapter;
|
|
91
|
+
closeIdempotencyKey: string;
|
|
91
92
|
unsubscribe: () => void;
|
|
92
93
|
reconnectUnsubscribe: () => void;
|
|
93
94
|
/** Per-session frame work queue; callbacks never race prompt ownership. */
|
|
@@ -571,8 +572,10 @@ export class AcpAgent implements Agent {
|
|
|
571
572
|
readonly #attaching = new Map<string, PendingAttachment>();
|
|
572
573
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
573
574
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
575
|
+
readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
|
|
574
576
|
readonly #sessionEpochs = new Map<string, number>();
|
|
575
577
|
readonly #tearingDown = new Map<string, number>();
|
|
578
|
+
readonly #closing = new Map<string, Promise<CloseSessionResponse>>();
|
|
576
579
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
577
580
|
#broker: Promise<BrokerConnection> | undefined;
|
|
578
581
|
readonly #startupOptions: AcpStartupOptions | undefined;
|
|
@@ -730,19 +733,21 @@ export class AcpAgent implements Agent {
|
|
|
730
733
|
return paginateAcpSessions(listed, params.cwd ?? undefined, this.#cursor(params.cursor));
|
|
731
734
|
}
|
|
732
735
|
|
|
733
|
-
|
|
736
|
+
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
734
737
|
const record = this.#sessions.get(params.sessionId);
|
|
735
738
|
const cwd = record?.cwd ?? this.#knownSessionCwds.get(params.sessionId);
|
|
736
739
|
// ACP close has no cwd. Only connection-owned sessions may reach broker lifecycle control.
|
|
737
|
-
if (!cwd) return {};
|
|
738
|
-
this.#
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
this.#
|
|
745
|
-
}
|
|
740
|
+
if (!cwd) return Promise.resolve({});
|
|
741
|
+
const existing = this.#closing.get(params.sessionId);
|
|
742
|
+
if (existing) return existing;
|
|
743
|
+
const deferred = Promise.withResolvers<CloseSessionResponse>();
|
|
744
|
+
this.#closing.set(params.sessionId, deferred.promise);
|
|
745
|
+
void this.#closeOwnedSession(params.sessionId).then(deferred.resolve, deferred.reject);
|
|
746
|
+
const cleanup = deferred.promise.finally(() => {
|
|
747
|
+
if (this.#closing.get(params.sessionId) === deferred.promise) this.#closing.delete(params.sessionId);
|
|
748
|
+
});
|
|
749
|
+
void cleanup.catch(() => undefined);
|
|
750
|
+
return deferred.promise;
|
|
746
751
|
}
|
|
747
752
|
|
|
748
753
|
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
|
@@ -932,6 +937,15 @@ export class AcpAgent implements Agent {
|
|
|
932
937
|
);
|
|
933
938
|
}
|
|
934
939
|
|
|
940
|
+
#isDefinitiveBrokerResponse(error: unknown): boolean {
|
|
941
|
+
// Response-derived client errors retain the broker error as details. Responses
|
|
942
|
+
// that represent ongoing or ambiguous lifecycle work must keep their key.
|
|
943
|
+
if (!(error instanceof SdkClientError)) return false;
|
|
944
|
+
const details = object(error.details);
|
|
945
|
+
if (details?.code !== error.code || details.message !== error.message) return false;
|
|
946
|
+
return !["terminal_uncertain", "cleanup_pending", "broker_restarting", "unavailable"].includes(error.code);
|
|
947
|
+
}
|
|
948
|
+
|
|
935
949
|
async #attachExisting(id: string, cwd: string): Promise<void> {
|
|
936
950
|
const epoch = this.#sessionEpoch(id);
|
|
937
951
|
const attached = this.#sessions.get(id);
|
|
@@ -1045,6 +1059,7 @@ export class AcpAgent implements Agent {
|
|
|
1045
1059
|
const record: SessionRecord = {
|
|
1046
1060
|
cwd,
|
|
1047
1061
|
adapter,
|
|
1062
|
+
closeIdempotencyKey: randomUUID(),
|
|
1048
1063
|
unsubscribe: () => {},
|
|
1049
1064
|
reconnectUnsubscribe: () => {},
|
|
1050
1065
|
frameTail: Promise.resolve(),
|
|
@@ -1059,6 +1074,7 @@ export class AcpAgent implements Agent {
|
|
|
1059
1074
|
this.#knownSessionCwds.set(id, cwd);
|
|
1060
1075
|
await applyAcpPermissionMode(adapter, this.#clientCapabilities);
|
|
1061
1076
|
this.#assertSessionEpoch(id, epoch);
|
|
1077
|
+
this.#pendingCloseIdempotencyKeys.delete(id);
|
|
1062
1078
|
} catch (error) {
|
|
1063
1079
|
if (adapter && this.#sessions.get(id)?.adapter === adapter) {
|
|
1064
1080
|
try {
|
|
@@ -1103,6 +1119,21 @@ export class AcpAgent implements Agent {
|
|
|
1103
1119
|
this.#knownSessionCwds.delete(id);
|
|
1104
1120
|
}
|
|
1105
1121
|
|
|
1122
|
+
async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
|
|
1123
|
+
this.#beginTeardown(id);
|
|
1124
|
+
try {
|
|
1125
|
+
const attaching = this.#attaching.get(id);
|
|
1126
|
+
// The record is published before permission initialization. Let a canceled
|
|
1127
|
+
// provisional attachment retire it before selecting the generation key.
|
|
1128
|
+
if (attaching) await Promise.allSettled([attaching.task]);
|
|
1129
|
+
await this.#teardownSession(id, "closed", true);
|
|
1130
|
+
this.#knownSessionCwds.delete(id);
|
|
1131
|
+
return {};
|
|
1132
|
+
} finally {
|
|
1133
|
+
this.#finishTeardown(id);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1106
1137
|
/**
|
|
1107
1138
|
* All local session disposal follows one path: remove ownership and reject a
|
|
1108
1139
|
* waiting prompt before any awaited socket or broker work. A failed close is
|
|
@@ -1130,13 +1161,13 @@ export class AcpAgent implements Agent {
|
|
|
1130
1161
|
failures.push(error);
|
|
1131
1162
|
}
|
|
1132
1163
|
if (closeRemote) {
|
|
1164
|
+
const closeIdempotencyKey =
|
|
1165
|
+
record?.closeIdempotencyKey ?? this.#pendingCloseIdempotencyKeys.get(id) ?? randomUUID();
|
|
1166
|
+
this.#pendingCloseIdempotencyKeys.set(id, closeIdempotencyKey);
|
|
1133
1167
|
try {
|
|
1134
|
-
await (await this.#brokerAdapter()).global(
|
|
1135
|
-
"session.close",
|
|
1136
|
-
{ sessionId: id },
|
|
1137
|
-
this.#lifecycleIdempotencyKey(id, "session.close"),
|
|
1138
|
-
);
|
|
1168
|
+
await (await this.#brokerAdapter()).global("session.close", { sessionId: id }, closeIdempotencyKey);
|
|
1139
1169
|
} catch (error) {
|
|
1170
|
+
if (this.#isDefinitiveBrokerResponse(error)) this.#pendingCloseIdempotencyKeys.delete(id);
|
|
1140
1171
|
if (!(ownershipBound && this.#isAlreadyGone(error))) failures.push(error);
|
|
1141
1172
|
}
|
|
1142
1173
|
}
|
|
@@ -1146,6 +1177,7 @@ export class AcpAgent implements Agent {
|
|
|
1146
1177
|
.join("; ");
|
|
1147
1178
|
throw aggregateAcpFailure("terminal_uncertain", `ACP session cleanup is uncertain: ${detail}`, failures);
|
|
1148
1179
|
}
|
|
1180
|
+
if (closeRemote) this.#pendingCloseIdempotencyKeys.delete(id);
|
|
1149
1181
|
} finally {
|
|
1150
1182
|
this.#finishTeardown(id);
|
|
1151
1183
|
}
|
|
@@ -1494,6 +1526,8 @@ export class AcpAgent implements Agent {
|
|
|
1494
1526
|
this.#attaching.clear();
|
|
1495
1527
|
this.#resolvingExisting.clear();
|
|
1496
1528
|
this.#knownSessionCwds.clear();
|
|
1529
|
+
this.#pendingCloseIdempotencyKeys.clear();
|
|
1530
|
+
if (this.#closing.size === 0) this.#closing.clear();
|
|
1497
1531
|
this.#tearingDown.clear();
|
|
1498
1532
|
if (this.#broker) {
|
|
1499
1533
|
const broker = this.#broker;
|
|
@@ -345,6 +345,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
345
345
|
#selectedActionIndex: number = 0;
|
|
346
346
|
#pendingThinkingChoice?: PendingThinkingChoice;
|
|
347
347
|
#selectedThinkingIndex: number = 0;
|
|
348
|
+
#assignmentState: "idle" | "assigning" = "idle";
|
|
349
|
+
#closeAfterAssignment = false;
|
|
348
350
|
|
|
349
351
|
// Preset landing state
|
|
350
352
|
#viewMode: ModelSelectorViewMode = "presets";
|
|
@@ -1493,6 +1495,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
1493
1495
|
}
|
|
1494
1496
|
|
|
1495
1497
|
handleInput(keyData: string): void {
|
|
1498
|
+
if (this.#assignmentState === "assigning") {
|
|
1499
|
+
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
1500
|
+
this.#closeAfterAssignment = true;
|
|
1501
|
+
}
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1496
1504
|
if (this.#pendingThinkingChoice) {
|
|
1497
1505
|
this.#handleThinkingMenuInput(keyData);
|
|
1498
1506
|
return;
|
|
@@ -1861,24 +1869,50 @@ export class ModelSelectorComponent extends Container {
|
|
|
1861
1869
|
? item.selector
|
|
1862
1870
|
: formatModelSelectorValue(item.selector, selectedThinkingLevel);
|
|
1863
1871
|
|
|
1864
|
-
|
|
1865
|
-
for (const targetRole of roles ?? [role]) {
|
|
1866
|
-
this.#roles[targetRole] = { model: item.model, thinkingLevel: selectedThinkingLevel };
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
// Notify caller (for updating agent state if needed)
|
|
1870
|
-
this.#onSelectCallback({
|
|
1872
|
+
const selection: Extract<ModelSelectorSelection, { kind: "assignment" }> = {
|
|
1871
1873
|
kind: "assignment",
|
|
1872
1874
|
model: item.model,
|
|
1873
1875
|
role,
|
|
1874
1876
|
roles,
|
|
1875
1877
|
thinkingLevel: selectedThinkingLevel,
|
|
1876
1878
|
selector: selectorValue,
|
|
1877
|
-
}
|
|
1879
|
+
};
|
|
1880
|
+
if (this.#isTrackedSingleAssignment(selection)) {
|
|
1881
|
+
void this.#handleTrackedAssignment(selection);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Update local state for UI
|
|
1886
|
+
for (const targetRole of roles ?? [role]) {
|
|
1887
|
+
this.#roles[targetRole] = { model: item.model, thinkingLevel: selectedThinkingLevel };
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Notify caller (for updating agent state if needed)
|
|
1891
|
+
this.#onSelectCallback(selection);
|
|
1878
1892
|
|
|
1879
1893
|
// Update list to show new badges
|
|
1880
1894
|
this.#updateList();
|
|
1881
1895
|
}
|
|
1896
|
+
#isTrackedSingleAssignment(selection: Extract<ModelSelectorSelection, { kind: "assignment" }>): boolean {
|
|
1897
|
+
return selection.role !== null && selection.roles === undefined;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
async #handleTrackedAssignment(selection: Extract<ModelSelectorSelection, { kind: "assignment" }>): Promise<void> {
|
|
1901
|
+
if (this.#assignmentState !== "idle") return;
|
|
1902
|
+
this.#assignmentState = "assigning";
|
|
1903
|
+
this.#tui.requestRender();
|
|
1904
|
+
try {
|
|
1905
|
+
await Promise.resolve(this.#onSelectCallback(selection));
|
|
1906
|
+
} catch {
|
|
1907
|
+
// The controller reports tracked assignment failures before rethrowing.
|
|
1908
|
+
} finally {
|
|
1909
|
+
this.#assignmentState = "idle";
|
|
1910
|
+
const shouldClose = this.#closeAfterAssignment;
|
|
1911
|
+
this.#closeAfterAssignment = false;
|
|
1912
|
+
if (shouldClose) this.#onCancelCallback();
|
|
1913
|
+
else this.#tui.requestRender();
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1882
1916
|
|
|
1883
1917
|
getSearchInput(): Input {
|
|
1884
1918
|
return this.#searchInput;
|
|
@@ -122,7 +122,7 @@ export class PlanPreviewOverlay extends Container {
|
|
|
122
122
|
? `${this.#inputKind === "notes" ? "Notes" : `Comment for source line ${this.#sourceLine}`} (Enter save, Esc cancel): ${this.#input.getValue()}`
|
|
123
123
|
: this.#focus === "sourceSelect"
|
|
124
124
|
? `Source line ${this.#sourceLine}/${this.lines.length || 1} j/k:select c:comment Esc:done`
|
|
125
|
-
: `${this.#scroll + 1}-${Math.min(this.#scroll + height, body.length)}/${body.length || 1} s:source line n:notes ${this.options.externalEditorKey ? `${this.options.externalEditorKey
|
|
125
|
+
: `${this.#scroll + 1}-${Math.min(this.#scroll + height, body.length)}/${body.length || 1} s:source line n:notes ${this.options.externalEditorKey ? `${this.options.externalEditorKey}:edit ` : ""}Tab:actions PgUp/PgDn:page`;
|
|
126
126
|
|
|
127
127
|
const rendered = [
|
|
128
128
|
new DynamicBorder().render(width)[0] ?? "",
|
|
@@ -60,6 +60,7 @@ type WizardStep =
|
|
|
60
60
|
*/
|
|
61
61
|
export interface MCPAddWizardOAuthResult {
|
|
62
62
|
credentialId: string;
|
|
63
|
+
tokenEndpoint: string;
|
|
63
64
|
clientId?: string;
|
|
64
65
|
clientSecret?: string;
|
|
65
66
|
}
|
|
@@ -122,6 +123,7 @@ export class MCPAddWizard extends Container {
|
|
|
122
123
|
#onCancelCallback: () => void;
|
|
123
124
|
#onOAuthCallback:
|
|
124
125
|
| ((
|
|
126
|
+
endpointUrl: string,
|
|
125
127
|
authUrl: string,
|
|
126
128
|
tokenUrl: string,
|
|
127
129
|
clientId: string,
|
|
@@ -141,6 +143,7 @@ export class MCPAddWizard extends Container {
|
|
|
141
143
|
onComplete: (name: string, config: MCPServerConfig, scope: Scope) => void,
|
|
142
144
|
onCancel: () => void,
|
|
143
145
|
onOAuth?: (
|
|
146
|
+
endpointUrl: string,
|
|
144
147
|
authUrl: string,
|
|
145
148
|
tokenUrl: string,
|
|
146
149
|
clientId: string,
|
|
@@ -1189,6 +1192,7 @@ export class MCPAddWizard extends Container {
|
|
|
1189
1192
|
try {
|
|
1190
1193
|
// Call OAuth handler
|
|
1191
1194
|
const oauthResult = await this.#onOAuthCallback(
|
|
1195
|
+
this.#state.url,
|
|
1192
1196
|
this.#state.oauthAuthUrl,
|
|
1193
1197
|
this.#state.oauthTokenUrl,
|
|
1194
1198
|
this.#state.oauthClientId,
|
|
@@ -1200,6 +1204,7 @@ export class MCPAddWizard extends Container {
|
|
|
1200
1204
|
// Store credential ID + any dynamically-registered client credentials,
|
|
1201
1205
|
// so the final mcp.json entry persists everything needed for refresh.
|
|
1202
1206
|
this.#state.oauthCredentialId = oauthResult.credentialId;
|
|
1207
|
+
this.#state.oauthTokenUrl = oauthResult.tokenEndpoint;
|
|
1203
1208
|
if (oauthResult.clientId) this.#state.oauthClientId = oauthResult.clientId;
|
|
1204
1209
|
if (oauthResult.clientSecret) this.#state.oauthClientSecret = oauthResult.clientSecret;
|
|
1205
1210
|
|
|
@@ -3,8 +3,12 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
4
4
|
import { formatCount, getProjectDir } from "@gajae-code/utils";
|
|
5
5
|
import { $ } from "bun";
|
|
6
|
-
import
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
type AppKeybinding,
|
|
8
|
+
KEYBINDINGS,
|
|
9
|
+
type KeybindingsManager,
|
|
10
|
+
type KeyDisplayContext,
|
|
11
|
+
} from "../../config/keybindings";
|
|
8
12
|
import { settings } from "../../config/settings";
|
|
9
13
|
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
|
|
10
14
|
import { theme } from "../../modes/theme/theme";
|
|
@@ -56,6 +60,7 @@ export interface StatusLineComponentOptions {
|
|
|
56
60
|
actionRegistry?: ActionRegistry<void>;
|
|
57
61
|
getKeybindings?: () => KeybindingsManager;
|
|
58
62
|
focusDomain?: FocusDomain;
|
|
63
|
+
keyDisplayContext?: KeyDisplayContext;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
export interface StatusLineActionHint {
|
|
@@ -93,6 +98,7 @@ export function getAvailableActionHints(
|
|
|
93
98
|
getKeybindings: (() => KeybindingsManager) | undefined,
|
|
94
99
|
width: number,
|
|
95
100
|
domain: FocusDomain = "composer",
|
|
101
|
+
keyDisplayContext?: KeyDisplayContext,
|
|
96
102
|
): StatusLineActionHint[] {
|
|
97
103
|
if (!actionRegistry || !getKeybindings || width <= 0) return [];
|
|
98
104
|
const keybindings = getKeybindings();
|
|
@@ -109,7 +115,9 @@ export function getAvailableActionHints(
|
|
|
109
115
|
if (!(bindingId in KEYBINDINGS)) continue;
|
|
110
116
|
const keys = keybindings.getKeys(bindingId);
|
|
111
117
|
if (keys.length === 0) continue;
|
|
112
|
-
const content =
|
|
118
|
+
const content =
|
|
119
|
+
theme.fg("dim", keybindings.getDisplayString(bindingId, keyDisplayContext)) +
|
|
120
|
+
theme.fg("muted", ` ${action.title}`);
|
|
113
121
|
const nextWidth = visibleWidth(content) + (selected.length === 0 ? 0 : 3);
|
|
114
122
|
if (used + nextWidth > width) break;
|
|
115
123
|
selected.push({ id: action.id, content });
|
|
@@ -161,6 +169,7 @@ export class StatusLineComponent implements Component {
|
|
|
161
169
|
#actionRegistry: ActionRegistry<void> | undefined;
|
|
162
170
|
#getKeybindings: (() => KeybindingsManager) | undefined;
|
|
163
171
|
#focusDomain: FocusDomain;
|
|
172
|
+
#keyDisplayContext: KeyDisplayContext | undefined;
|
|
164
173
|
|
|
165
174
|
#resolvedSettingsCache:
|
|
166
175
|
| (Required<Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">> &
|
|
@@ -209,6 +218,7 @@ export class StatusLineComponent implements Component {
|
|
|
209
218
|
this.#actionRegistry = options.actionRegistry;
|
|
210
219
|
this.#getKeybindings = options.getKeybindings;
|
|
211
220
|
this.#focusDomain = options.focusDomain ?? "composer";
|
|
221
|
+
this.#keyDisplayContext = options.keyDisplayContext;
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
updateSettings(settings: StatusLineSettings): void {
|
|
@@ -796,7 +806,13 @@ export class StatusLineComponent implements Component {
|
|
|
796
806
|
const actionHints =
|
|
797
807
|
effectiveSettings.showActionHints === false
|
|
798
808
|
? []
|
|
799
|
-
: getAvailableActionHints(
|
|
809
|
+
: getAvailableActionHints(
|
|
810
|
+
this.#actionRegistry,
|
|
811
|
+
this.#getKeybindings,
|
|
812
|
+
width,
|
|
813
|
+
this.#focusDomain,
|
|
814
|
+
this.#keyDisplayContext,
|
|
815
|
+
);
|
|
800
816
|
for (const segId of effectiveSettings.rightSegments) {
|
|
801
817
|
const rendered = renderSegment(segId, ctx);
|
|
802
818
|
if (rendered.visible && rendered.content) {
|