@gajae-code/coding-agent 0.11.7 → 0.11.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 +73 -1
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli.d.ts +1 -0
- 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/model-profiles.d.ts +2 -3
- 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/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +44 -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/gjc-runtime/workflow-manifest.d.ts +5 -3
- 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 +4 -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/runtime-mcp/redaction.d.ts +2 -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 +7 -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 +15 -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 +22 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/skill-state/active-state.d.ts +4 -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/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +50 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commands/sdk.ts +2 -2
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.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-profiles.ts +2 -3
- 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 +80 -165
- 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/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +842 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- 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 +78 -4
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/state-writer.ts +166 -2
- 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/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/internal-urls/docs-index.generated.ts +8 -6
- 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 +32 -20
- 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 +70 -36
- 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 +7 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/json-rpc.ts +7 -4
- 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/redaction.ts +30 -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/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +20 -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 +15 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +853 -112
- 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 +138 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +998 -133
- package/src/session/internal/managed-session-storage.ts +268 -203
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +406 -360
- package/src/session/session-storage.ts +151 -11
- package/src/skill-state/active-state.ts +10 -0
- package/src/task/executor.ts +13 -5
- 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 +199 -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
|
@@ -5,7 +5,7 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
|
|
|
5
5
|
import type { Component, OverlayHandle, SlashCommand } from "@gajae-code/tui";
|
|
6
6
|
import { Input, isPetMode, Loader, Spacer, Text } from "@gajae-code/tui";
|
|
7
7
|
import { getAgentDbPath, getProjectDir, logger, VERSION } from "@gajae-code/utils";
|
|
8
|
-
import {
|
|
8
|
+
import type { AppKeybinding } from "../../config/keybindings";
|
|
9
9
|
import {
|
|
10
10
|
activateModelProfile,
|
|
11
11
|
type MaterializeModelProfileForDeletionResult,
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
import type { NotificationSessionStatus } from "../../sdk/bus/session-control";
|
|
65
65
|
import {
|
|
66
66
|
ensureTelegramDaemonRunningDetailed,
|
|
67
|
-
|
|
67
|
+
resolveTelegramSetupPreflight,
|
|
68
68
|
unregisterNotificationRoot,
|
|
69
69
|
} from "../../sdk/bus/telegram-daemon";
|
|
70
70
|
import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control";
|
|
@@ -226,6 +226,7 @@ export interface NotificationsEditorOperationDependencies {
|
|
|
226
226
|
sanitizeDiagnostic: typeof sanitizeDiagnostic;
|
|
227
227
|
ensureTelegramDaemonRunningDetailed: typeof ensureTelegramDaemonRunningDetailed;
|
|
228
228
|
runTelegramSetup: typeof runTelegramSetup;
|
|
229
|
+
resolveTelegramSetupPreflight: typeof resolveTelegramSetupPreflight;
|
|
229
230
|
proposedTelegramIdentity: typeof proposedTelegramIdentity;
|
|
230
231
|
reconcileCommittedTelegramConfiguration: typeof reconcileCommittedTelegramConfiguration;
|
|
231
232
|
saveTelegramInactive: typeof saveTelegramInactive;
|
|
@@ -250,6 +251,7 @@ const notificationEditorOperationDependencies: NotificationsEditorOperationDepen
|
|
|
250
251
|
sanitizeDiagnostic,
|
|
251
252
|
ensureTelegramDaemonRunningDetailed,
|
|
252
253
|
runTelegramSetup,
|
|
254
|
+
resolveTelegramSetupPreflight,
|
|
253
255
|
proposedTelegramIdentity,
|
|
254
256
|
reconcileCommittedTelegramConfiguration,
|
|
255
257
|
saveTelegramInactive,
|
|
@@ -306,33 +308,8 @@ export function createNotificationsEditorOperations(
|
|
|
306
308
|
cwd: ctx.sessionManager.getCwd(),
|
|
307
309
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
308
310
|
});
|
|
309
|
-
const telegramSetupPreflight = async (): Promise<TelegramSetupPreflight> =>
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const state = await readDaemonState(ctx.settings);
|
|
313
|
-
const validPid = Number.isSafeInteger(state?.pid) && (state?.pid ?? 0) > 0;
|
|
314
|
-
if (!state || !validPid) return { storedChatId };
|
|
315
|
-
let live = false;
|
|
316
|
-
try {
|
|
317
|
-
process.kill(state.pid, 0);
|
|
318
|
-
live = true;
|
|
319
|
-
} catch (error) {
|
|
320
|
-
live = (error as NodeJS.ErrnoException).code === "EPERM";
|
|
321
|
-
}
|
|
322
|
-
return live
|
|
323
|
-
? {
|
|
324
|
-
storedChatId,
|
|
325
|
-
daemon: {
|
|
326
|
-
live,
|
|
327
|
-
tokenFingerprint: typeof state.tokenFingerprint === "string" ? state.tokenFingerprint : undefined,
|
|
328
|
-
chatId: typeof state.chatId === "string" && state.chatId.trim() ? state.chatId.trim() : undefined,
|
|
329
|
-
},
|
|
330
|
-
}
|
|
331
|
-
: { storedChatId };
|
|
332
|
-
} catch {
|
|
333
|
-
return { storedChatId };
|
|
334
|
-
}
|
|
335
|
-
};
|
|
311
|
+
const telegramSetupPreflight = async (): Promise<TelegramSetupPreflight> =>
|
|
312
|
+
await services.resolveTelegramSetupPreflight(ctx.settings);
|
|
336
313
|
|
|
337
314
|
return {
|
|
338
315
|
loadState: async () => {
|
|
@@ -838,7 +815,7 @@ export class SelectorController {
|
|
|
838
815
|
id: `action:${action.id}`,
|
|
839
816
|
label: action.label,
|
|
840
817
|
description: action.id,
|
|
841
|
-
keybinding:
|
|
818
|
+
keybinding: this.ctx.keybindings.getDisplayString(action.id as AppKeybinding) || undefined,
|
|
842
819
|
searchText: action.id,
|
|
843
820
|
handler: action.handler,
|
|
844
821
|
})),
|
|
@@ -1108,12 +1085,12 @@ export class SelectorController {
|
|
|
1108
1085
|
|
|
1109
1086
|
async #handleImageGenerationConfig(): Promise<void> {
|
|
1110
1087
|
const provider = await this.ctx.showHookInput(
|
|
1111
|
-
"Image Generation provider (auto, openai, gemini, openrouter, antigravity, custom)",
|
|
1088
|
+
"Image Generation provider (auto, openai, gemini, openrouter, antigravity, alibaba, custom)",
|
|
1112
1089
|
"auto",
|
|
1113
1090
|
);
|
|
1114
1091
|
if (provider === undefined) return;
|
|
1115
1092
|
const normalized = provider.trim().toLowerCase();
|
|
1116
|
-
const validProviders = ["auto", "openai", "gemini", "openrouter", "antigravity", "custom"];
|
|
1093
|
+
const validProviders = ["auto", "openai", "gemini", "openrouter", "antigravity", "alibaba", "custom"];
|
|
1117
1094
|
if (!validProviders.includes(normalized)) {
|
|
1118
1095
|
this.ctx.showStatus(`Invalid image provider: ${normalized}. Valid: ${validProviders.join(", ")}`);
|
|
1119
1096
|
return;
|
|
@@ -1145,7 +1122,14 @@ export class SelectorController {
|
|
|
1145
1122
|
if (scope === undefined) return;
|
|
1146
1123
|
const persistDefault = scope.trim().toLowerCase() === "default";
|
|
1147
1124
|
|
|
1148
|
-
const imageProvider = normalized as
|
|
1125
|
+
const imageProvider = normalized as
|
|
1126
|
+
| "auto"
|
|
1127
|
+
| "openai"
|
|
1128
|
+
| "gemini"
|
|
1129
|
+
| "openrouter"
|
|
1130
|
+
| "antigravity"
|
|
1131
|
+
| "alibaba"
|
|
1132
|
+
| "custom";
|
|
1149
1133
|
setPreferredImageProvider(imageProvider === "custom" ? "auto" : imageProvider);
|
|
1150
1134
|
setConfiguredImageModel({
|
|
1151
1135
|
provider: imageProvider,
|
|
@@ -1644,6 +1628,7 @@ export class SelectorController {
|
|
|
1644
1628
|
imgProvider === "gemini" ||
|
|
1645
1629
|
imgProvider === "openrouter" ||
|
|
1646
1630
|
imgProvider === "antigravity" ||
|
|
1631
|
+
imgProvider === "alibaba" ||
|
|
1647
1632
|
imgProvider === "custom"
|
|
1648
1633
|
) {
|
|
1649
1634
|
setPreferredImageProvider(imgProvider === "custom" ? "auto" : imgProvider);
|
|
@@ -1694,6 +1679,14 @@ export class SelectorController {
|
|
|
1694
1679
|
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
1695
1680
|
this.showSelector(done => {
|
|
1696
1681
|
let modelSelector: ModelSelectorComponent;
|
|
1682
|
+
const refreshRoleAssignments = () => {
|
|
1683
|
+
modelSelector.refreshRoleAssignments({
|
|
1684
|
+
currentModel: this.ctx.session.model,
|
|
1685
|
+
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
1686
|
+
activeModelProfile:
|
|
1687
|
+
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
1688
|
+
});
|
|
1689
|
+
};
|
|
1697
1690
|
modelSelector = new ModelSelectorComponent(
|
|
1698
1691
|
this.ctx.ui,
|
|
1699
1692
|
this.ctx.session.model,
|
|
@@ -1701,6 +1694,8 @@ export class SelectorController {
|
|
|
1701
1694
|
this.ctx.session.modelRegistry,
|
|
1702
1695
|
this.ctx.session.scopedModels,
|
|
1703
1696
|
async selection => {
|
|
1697
|
+
const isTrackedSingleAssignment =
|
|
1698
|
+
selection.kind === "assignment" && selection.role !== null && selection.roles === undefined;
|
|
1704
1699
|
try {
|
|
1705
1700
|
if (selection.kind === "createProfile") {
|
|
1706
1701
|
done();
|
|
@@ -1741,7 +1736,7 @@ export class SelectorController {
|
|
|
1741
1736
|
this.ctx.showStatus(`Temporary model: ${selectedSelector ?? model.id}`);
|
|
1742
1737
|
done();
|
|
1743
1738
|
this.ctx.ui.requestRender();
|
|
1744
|
-
} else if (selection.roles) {
|
|
1739
|
+
} else if (selection.roles !== undefined) {
|
|
1745
1740
|
const targetRoles: readonly GjcModelAssignmentTargetId[] = selection.roles;
|
|
1746
1741
|
const includesDefault = targetRoles.includes("default");
|
|
1747
1742
|
const includesRoleAgent = targetRoles.some(targetRole => targetRole !== "default");
|
|
@@ -1819,25 +1814,23 @@ export class SelectorController {
|
|
|
1819
1814
|
selectedSelector ?? `${model.provider}/${model.id}`,
|
|
1820
1815
|
thinkingLevel,
|
|
1821
1816
|
);
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1817
|
+
if (
|
|
1818
|
+
!materializeActiveModelProfileAssignment({
|
|
1819
|
+
session: this.ctx.session,
|
|
1820
|
+
settings: this.ctx.settings,
|
|
1821
|
+
role,
|
|
1822
|
+
selector: value,
|
|
1823
|
+
})
|
|
1824
|
+
) {
|
|
1825
|
+
this.ctx.settings.setModelRole(role, value);
|
|
1826
|
+
}
|
|
1828
1827
|
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
|
1829
1828
|
this.ctx.session.setThinkingLevel(thinkingLevel);
|
|
1830
1829
|
}
|
|
1831
|
-
|
|
1832
|
-
currentModel: this.ctx.session.model,
|
|
1833
|
-
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
1834
|
-
activeModelProfile:
|
|
1835
|
-
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
1836
|
-
});
|
|
1830
|
+
refreshRoleAssignments();
|
|
1837
1831
|
this.ctx.statusLine.invalidate();
|
|
1838
1832
|
this.ctx.updateEditorBorderColor();
|
|
1839
1833
|
this.ctx.showStatus(`Default model: ${selectedSelector ?? model.id}`);
|
|
1840
|
-
done();
|
|
1841
1834
|
this.ctx.ui.requestRender();
|
|
1842
1835
|
} else {
|
|
1843
1836
|
const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
|
|
@@ -1860,22 +1853,21 @@ export class SelectorController {
|
|
|
1860
1853
|
this.ctx.settings.setAgentModelOverride(role, value);
|
|
1861
1854
|
}
|
|
1862
1855
|
}
|
|
1863
|
-
|
|
1864
|
-
currentModel: this.ctx.session.model,
|
|
1865
|
-
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
1866
|
-
activeModelProfile:
|
|
1867
|
-
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
1868
|
-
});
|
|
1856
|
+
refreshRoleAssignments();
|
|
1869
1857
|
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
1870
1858
|
this.ctx.statusLine.invalidate();
|
|
1871
1859
|
this.ctx.updateEditorBorderColor();
|
|
1872
1860
|
await this.ctx.notifyConfigChanged?.();
|
|
1873
1861
|
this.ctx.showStatus(`${role} agent model: ${value}`);
|
|
1874
|
-
done();
|
|
1875
1862
|
this.ctx.ui.requestRender();
|
|
1876
1863
|
}
|
|
1877
1864
|
} catch (error) {
|
|
1878
1865
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1866
|
+
if (isTrackedSingleAssignment) {
|
|
1867
|
+
refreshRoleAssignments();
|
|
1868
|
+
this.ctx.ui.requestRender();
|
|
1869
|
+
throw error;
|
|
1870
|
+
}
|
|
1879
1871
|
}
|
|
1880
1872
|
},
|
|
1881
1873
|
() => {
|
|
@@ -16,7 +16,13 @@ import {
|
|
|
16
16
|
import { APP_NAME, adjustHsv, getProjectDir, logger, postmortem } from "@gajae-code/utils";
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { AsyncJobManager } from "../async";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type AppKeybinding,
|
|
21
|
+
formatKeyHint,
|
|
22
|
+
formatKeyHints,
|
|
23
|
+
KeybindingsManager,
|
|
24
|
+
type KeyDisplayContext,
|
|
25
|
+
} from "../config/keybindings";
|
|
20
26
|
import { isSettingsInitialized, type Settings, settings } from "../config/settings";
|
|
21
27
|
import { DEFAULT_GJC_DEFINITION_NAMES } from "../defaults/gjc-defaults";
|
|
22
28
|
import type {
|
|
@@ -64,6 +70,7 @@ import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
|
64
70
|
import { StatusLineComponent } from "./components/tool-status-header";
|
|
65
71
|
import { composeToolText } from "./components/tool-transcript-format";
|
|
66
72
|
import {
|
|
73
|
+
type RecentSession,
|
|
67
74
|
WelcomeComponent,
|
|
68
75
|
type WelcomeLogoMode,
|
|
69
76
|
type LspServerInfo as WelcomeLspServerInfo,
|
|
@@ -83,6 +90,7 @@ import { TodoCommandController } from "./controllers/todo-command-controller";
|
|
|
83
90
|
import { IrcObservationLedger } from "./irc-observation-ledger";
|
|
84
91
|
import { JobsObserver } from "./jobs-observer";
|
|
85
92
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
93
|
+
import { PromptSuggestionController } from "./prompt-suggestion-controller";
|
|
86
94
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
87
95
|
import { interruptHint } from "./shared";
|
|
88
96
|
import { shouldShowExtensionCommand } from "./slash-command-visibility";
|
|
@@ -105,8 +113,15 @@ import {
|
|
|
105
113
|
import type { ParsedIrcMessage } from "./utils/irc-message";
|
|
106
114
|
import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
export function getDefaultComposerPlaceholder(context: KeyDisplayContext = { platform: process.platform }): string {
|
|
117
|
+
const newlineKeys = context.platform === "win32" ? ["alt+enter", "ctrl+j"] : ["shift+enter", "ctrl+j"];
|
|
118
|
+
return `Type your message... ${formatKeyHints(newlineKeys, context)}: New line · ${formatKeyHint(
|
|
119
|
+
"ctrl+c",
|
|
120
|
+
context,
|
|
121
|
+
)}: Clear · ${formatKeyHint("ctrl+r", context)}: Search history · ${formatKeyHint("shift+tab", context)}: Reasoning`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
|
|
110
125
|
const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
|
|
111
126
|
|
|
112
127
|
const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
|
|
@@ -128,23 +143,6 @@ export function getWelcomeTranscriptReservedRows(chatContainer: Container, width
|
|
|
128
143
|
? 0
|
|
129
144
|
: chatContainer.render(width).length;
|
|
130
145
|
}
|
|
131
|
-
const FRIENDLY_KEY_PARTS: Record<string, string> = {
|
|
132
|
-
alt: "Alt",
|
|
133
|
-
cmd: "Cmd",
|
|
134
|
-
command: "Cmd",
|
|
135
|
-
ctrl: "Ctrl",
|
|
136
|
-
enter: "Enter",
|
|
137
|
-
meta: process.platform === "darwin" ? "Command" : "Meta",
|
|
138
|
-
option: "Option",
|
|
139
|
-
shift: "Shift",
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
function formatShortcutForPlaceholder(key: string): string {
|
|
143
|
-
return key
|
|
144
|
-
.split("+")
|
|
145
|
-
.map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part))
|
|
146
|
-
.join("+");
|
|
147
|
-
}
|
|
148
146
|
|
|
149
147
|
const HINT_SHIMMER_PALETTE: ShimmerPalette = {
|
|
150
148
|
low: "dim",
|
|
@@ -169,7 +167,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
|
|
|
169
167
|
editor.setClosedBorderBox(true);
|
|
170
168
|
editor.setPromptGutter(undefined);
|
|
171
169
|
editor.setInputPrefix(getDefaultInputPrefix());
|
|
172
|
-
editor.setPlaceholder(
|
|
170
|
+
editor.setPlaceholder(getDefaultComposerPlaceholder());
|
|
173
171
|
editor.setPaddingX(1);
|
|
174
172
|
editor.setRightGutterWidth(1);
|
|
175
173
|
editor.setTopBorder(undefined);
|
|
@@ -340,6 +338,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
340
338
|
fileSlashCommands: Set<string> = new Set();
|
|
341
339
|
skillCommands: Map<string, Skill> = new Map();
|
|
342
340
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
341
|
+
promptSuggestion: PromptSuggestionController;
|
|
343
342
|
|
|
344
343
|
#baseSlashCommands: SlashCommand[] = [];
|
|
345
344
|
#resolvedSlashCommands: SlashCommand[] = [];
|
|
@@ -351,6 +350,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
351
350
|
#petUnavailableWarningDisposer?: () => void;
|
|
352
351
|
readonly #version: string;
|
|
353
352
|
readonly #changelogMarkdown: string | undefined;
|
|
353
|
+
readonly #keyDisplayContext: KeyDisplayContext;
|
|
354
354
|
|
|
355
355
|
readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
|
|
356
356
|
mcpManager?: import("../runtime-mcp").MCPManager;
|
|
@@ -398,6 +398,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
398
398
|
lspServers: LspStartupServerInfo[] | undefined = undefined,
|
|
399
399
|
mcpManager?: import("../runtime-mcp").MCPManager,
|
|
400
400
|
eventBus?: EventBus,
|
|
401
|
+
keyDisplayContext: KeyDisplayContext = { platform: process.platform },
|
|
401
402
|
) {
|
|
402
403
|
this.session = session;
|
|
403
404
|
this.sessionManager = session.sessionManager;
|
|
@@ -436,6 +437,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
436
437
|
this.lspServers = lspServers;
|
|
437
438
|
this.mcpManager = mcpManager;
|
|
438
439
|
this.#eventBus = eventBus;
|
|
440
|
+
this.#keyDisplayContext = keyDisplayContext;
|
|
441
|
+
this.keybindings.setDisplayContext(this.#keyDisplayContext);
|
|
439
442
|
const thisMode = this;
|
|
440
443
|
this.#goalModeController = new GoalModeController({
|
|
441
444
|
session: this.session,
|
|
@@ -517,6 +520,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
517
520
|
this.btwContainer = new Container();
|
|
518
521
|
this.editor = new CustomEditor(getEditorTheme());
|
|
519
522
|
configureDefaultComposerChrome(this.editor);
|
|
523
|
+
this.editor.setPlaceholder(this.#getComposerPlaceholder());
|
|
520
524
|
this.editor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
521
525
|
this.editor.setAutocompleteMaxVisible(settings.get("autocompleteMaxVisible"));
|
|
522
526
|
this.editor.onAutocompleteCancel = () => {
|
|
@@ -544,7 +548,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
544
548
|
this.hookWidgetContainerBelow = new Container();
|
|
545
549
|
this.editorContainer = new Container();
|
|
546
550
|
this.editorContainer.addChild(this.editor);
|
|
547
|
-
this.statusLine = new StatusLineComponent(session, {
|
|
551
|
+
this.statusLine = new StatusLineComponent(session, {
|
|
552
|
+
version: this.#version,
|
|
553
|
+
focusDomain: "composer",
|
|
554
|
+
keyDisplayContext: this.#keyDisplayContext,
|
|
555
|
+
});
|
|
548
556
|
this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
|
|
549
557
|
|
|
550
558
|
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
|
@@ -580,6 +588,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
580
588
|
this.#selectorController = new SelectorController(this);
|
|
581
589
|
this.#inputController = new InputController(this);
|
|
582
590
|
this.statusLine.setActionRegistry(this.#inputController.actionRegistry, () => this.keybindings);
|
|
591
|
+
this.promptSuggestion = new PromptSuggestionController(this);
|
|
583
592
|
this.#observerRegistry = new SessionObserverRegistry();
|
|
584
593
|
}
|
|
585
594
|
|
|
@@ -600,6 +609,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
600
609
|
if (this.isInitialized) return;
|
|
601
610
|
|
|
602
611
|
this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
|
|
612
|
+
this.keybindings.setDisplayContext(this.#keyDisplayContext);
|
|
603
613
|
|
|
604
614
|
// Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
|
|
605
615
|
this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
|
|
@@ -622,15 +632,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
622
632
|
const modelName = this.session.model?.name ?? "Unknown";
|
|
623
633
|
const providerName = this.session.model?.provider ?? "Unknown";
|
|
624
634
|
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
sessions.map(s => ({
|
|
629
|
-
name: s.name,
|
|
630
|
-
timeAgo: s.timeAgo,
|
|
631
|
-
})),
|
|
632
|
-
),
|
|
633
|
-
);
|
|
635
|
+
// Session history is display-only. Never hold the editor and keyboard behind
|
|
636
|
+
// transcript I/O; populate the welcome trail after the interactive surface exists.
|
|
637
|
+
const recentSessions: RecentSession[] = [];
|
|
634
638
|
|
|
635
639
|
const startupQuiet = settings.get("startup.quiet");
|
|
636
640
|
const welcomeLogoMode = resolveWelcomeLogoMode(settings.get("startup.welcomeBannerMode"));
|
|
@@ -657,6 +661,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
657
661
|
getReservedBottomRows: getWelcomeReservedBottomRows,
|
|
658
662
|
changelogMarkdown: this.#changelogMarkdown,
|
|
659
663
|
collapseChangelog: settings.get("collapseChangelog"),
|
|
664
|
+
keyDisplayContext: this.#keyDisplayContext,
|
|
660
665
|
},
|
|
661
666
|
);
|
|
662
667
|
|
|
@@ -753,6 +758,31 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
753
758
|
if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
|
|
754
759
|
this.#syncIrcSidebarAvailabilityFromSettings();
|
|
755
760
|
this.ui.requestRender(true);
|
|
761
|
+
if (this.#welcomeComponent) {
|
|
762
|
+
const welcomeComponent = this.#welcomeComponent;
|
|
763
|
+
const recentSessionsTimer = setTimeout(() => {
|
|
764
|
+
void logger
|
|
765
|
+
.time("InteractiveMode.init:recentSessions", () =>
|
|
766
|
+
getRecentSessions(this.sessionManager.getSessionDir()),
|
|
767
|
+
)
|
|
768
|
+
.then(sessions => {
|
|
769
|
+
if (this.#welcomeComponent !== welcomeComponent) return;
|
|
770
|
+
welcomeComponent.setRecentSessions(
|
|
771
|
+
sessions.map(session => ({
|
|
772
|
+
name: session.name,
|
|
773
|
+
timeAgo: session.timeAgo,
|
|
774
|
+
})),
|
|
775
|
+
);
|
|
776
|
+
this.ui.requestRender();
|
|
777
|
+
})
|
|
778
|
+
.catch(error => {
|
|
779
|
+
logger.debug("Failed to load recent sessions for welcome screen", {
|
|
780
|
+
error: error instanceof Error ? error.message : String(error),
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
}, 0);
|
|
784
|
+
recentSessionsTimer.unref?.();
|
|
785
|
+
}
|
|
756
786
|
|
|
757
787
|
// GitHub star reminder (interactive-only). Register the decline-driven
|
|
758
788
|
// injection contributor and schedule the launch nudge after the first
|
|
@@ -807,6 +837,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
807
837
|
onThemeChange(() => {
|
|
808
838
|
clearRenderCache();
|
|
809
839
|
configureDefaultComposerChrome(this.editor);
|
|
840
|
+
this.editor.setPlaceholder(this.#getComposerPlaceholder());
|
|
810
841
|
this.ui.invalidate();
|
|
811
842
|
this.updateEditorChrome();
|
|
812
843
|
this.ui.requestRender();
|
|
@@ -1038,12 +1069,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1038
1069
|
}
|
|
1039
1070
|
|
|
1040
1071
|
#getComposerPlaceholder(): string {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1072
|
+
const defaultPlaceholder = getDefaultComposerPlaceholder(this.#keyDisplayContext);
|
|
1073
|
+
if (!this.#isPromptDeliveryBusy()) return defaultPlaceholder;
|
|
1074
|
+
const submitAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
|
|
1075
|
+
const submitKey = this.keybindings.getDisplayString("tui.input.submit", this.#keyDisplayContext);
|
|
1076
|
+
const parts = submitKey ? [`${submitKey}: ${submitAction}`] : [];
|
|
1044
1077
|
const queueKey = this.#getMessageQueueShortcut();
|
|
1045
|
-
if (queueKey) parts.push(`${
|
|
1046
|
-
return `${
|
|
1078
|
+
if (queueKey) parts.push(`${formatKeyHint(queueKey, this.#keyDisplayContext)}: Queue`);
|
|
1079
|
+
return parts.length > 0 ? `${defaultPlaceholder} · ${parts.join(" · ")}` : defaultPlaceholder;
|
|
1047
1080
|
}
|
|
1048
1081
|
|
|
1049
1082
|
#getWelcomeReservedRows(width: number): number {
|
|
@@ -1398,6 +1431,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1398
1431
|
: new CustomEditor(getEditorTheme());
|
|
1399
1432
|
|
|
1400
1433
|
configureDefaultComposerChrome(nextEditor);
|
|
1434
|
+
nextEditor.setPlaceholder(this.#getComposerPlaceholder());
|
|
1401
1435
|
nextEditor.setUseTerminalCursor(this.ui.getShowHardwareCursor());
|
|
1402
1436
|
nextEditor.setAutocompleteMaxVisible(this.settings.get("autocompleteMaxVisible"));
|
|
1403
1437
|
nextEditor.onAutocompleteCancel = () => {
|
|
@@ -3,12 +3,10 @@ import {
|
|
|
3
3
|
type AutocompleteProvider,
|
|
4
4
|
CombinedAutocompleteProvider,
|
|
5
5
|
extractSlashCommandTokenPrefix,
|
|
6
|
-
getKeybindings,
|
|
7
6
|
getSlashCommandMatchRank,
|
|
8
7
|
type SlashCommand,
|
|
9
8
|
} from "@gajae-code/tui";
|
|
10
9
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
11
|
-
import { formatKeyHints } from "../config/keybindings";
|
|
12
10
|
import { isSettingsInitialized, settings } from "../config/settings";
|
|
13
11
|
import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
|
|
14
12
|
|
|
@@ -44,6 +42,11 @@ interface PromptActionAutocompleteOptions {
|
|
|
44
42
|
moveCursorToMessageStart: () => void;
|
|
45
43
|
moveCursorToLineStart: () => void;
|
|
46
44
|
moveCursorToLineEnd: () => void;
|
|
45
|
+
/**
|
|
46
|
+
* Ghost-text next-prompt prediction shown in the empty composer as a dim
|
|
47
|
+
* inline hint (Tab accepts it via the editor's onTab handler).
|
|
48
|
+
*/
|
|
49
|
+
getPromptSuggestion?: () => string | null;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
function fuzzyMatch(query: string, target: string): boolean {
|
|
@@ -186,11 +189,18 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
186
189
|
#baseProvider: CombinedAutocompleteProvider;
|
|
187
190
|
#actions: PromptActionDefinition[];
|
|
188
191
|
#commands: SlashCommand[];
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
#getPromptSuggestion: (() => string | null) | undefined;
|
|
193
|
+
|
|
194
|
+
constructor(
|
|
195
|
+
commands: SlashCommand[],
|
|
196
|
+
basePath: string,
|
|
197
|
+
actions: PromptActionDefinition[],
|
|
198
|
+
getPromptSuggestion?: () => string | null,
|
|
199
|
+
) {
|
|
191
200
|
this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
|
|
192
201
|
this.#actions = actions;
|
|
193
202
|
this.#commands = commands;
|
|
203
|
+
this.#getPromptSuggestion = getPromptSuggestion;
|
|
194
204
|
}
|
|
195
205
|
|
|
196
206
|
async getSuggestions(
|
|
@@ -309,6 +319,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
309
319
|
}
|
|
310
320
|
|
|
311
321
|
getInlineHint(lines: string[], cursorLine: number, cursorCol: number): string | null {
|
|
322
|
+
// An empty composer renders the pending prompt suggestion as ghost text.
|
|
323
|
+
if (lines.every(line => line === "")) {
|
|
324
|
+
const suggestion = this.#getPromptSuggestion?.();
|
|
325
|
+
if (suggestion) return suggestion;
|
|
326
|
+
}
|
|
312
327
|
return this.#baseProvider.getInlineHint?.(lines, cursorLine, cursorCol) ?? null;
|
|
313
328
|
}
|
|
314
329
|
trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
|
|
@@ -382,12 +397,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
382
397
|
export function createPromptActionAutocompleteProvider(
|
|
383
398
|
options: PromptActionAutocompleteOptions,
|
|
384
399
|
): PromptActionAutocompleteProvider {
|
|
385
|
-
const editorKeybindings = getKeybindings();
|
|
386
400
|
const actions: PromptActionDefinition[] = [
|
|
387
401
|
{
|
|
388
402
|
id: "new-session",
|
|
389
403
|
label: "Start new session",
|
|
390
|
-
description:
|
|
404
|
+
description: options.keybindings.getDisplayString("app.session.new"),
|
|
391
405
|
keywords: ["new", "session", "fresh", "clear", "start", "conversation"],
|
|
392
406
|
execute: options.newSession,
|
|
393
407
|
},
|
|
@@ -401,21 +415,21 @@ export function createPromptActionAutocompleteProvider(
|
|
|
401
415
|
{
|
|
402
416
|
id: "copy-line",
|
|
403
417
|
label: "Copy current line",
|
|
404
|
-
description:
|
|
418
|
+
description: options.keybindings.getDisplayString("app.clipboard.copyLine"),
|
|
405
419
|
keywords: ["copy", "line", "clipboard", "current"],
|
|
406
420
|
execute: options.copyCurrentLine,
|
|
407
421
|
},
|
|
408
422
|
{
|
|
409
423
|
id: "copy-prompt",
|
|
410
424
|
label: "Copy whole prompt",
|
|
411
|
-
description:
|
|
425
|
+
description: options.keybindings.getDisplayString("app.clipboard.copyPrompt"),
|
|
412
426
|
keywords: ["copy", "prompt", "clipboard", "message"],
|
|
413
427
|
execute: options.copyPrompt,
|
|
414
428
|
},
|
|
415
429
|
{
|
|
416
430
|
id: "paste-image",
|
|
417
431
|
label: "Paste image from clipboard",
|
|
418
|
-
description:
|
|
432
|
+
description: options.keybindings.getDisplayString("app.clipboard.pasteImage"),
|
|
419
433
|
keywords: ["paste", "image", "clipboard", "screenshot", "attach", "vision"],
|
|
420
434
|
execute: options.pasteImage,
|
|
421
435
|
},
|
|
@@ -429,7 +443,7 @@ export function createPromptActionAutocompleteProvider(
|
|
|
429
443
|
{
|
|
430
444
|
id: "undo",
|
|
431
445
|
label: "Undo",
|
|
432
|
-
description:
|
|
446
|
+
description: options.keybindings.getDisplayString("tui.editor.undo"),
|
|
433
447
|
keywords: ["undo", "revert", "edit", "history"],
|
|
434
448
|
execute: options.undo,
|
|
435
449
|
},
|
|
@@ -450,18 +464,23 @@ export function createPromptActionAutocompleteProvider(
|
|
|
450
464
|
{
|
|
451
465
|
id: "cursor-line-start",
|
|
452
466
|
label: "Move cursor to beginning of line",
|
|
453
|
-
description:
|
|
467
|
+
description: options.keybindings.getDisplayString("tui.editor.cursorLineStart"),
|
|
454
468
|
keywords: ["move", "cursor", "line", "start", "beginning", "home"],
|
|
455
469
|
execute: options.moveCursorToLineStart,
|
|
456
470
|
},
|
|
457
471
|
{
|
|
458
472
|
id: "cursor-line-end",
|
|
459
473
|
label: "Move cursor to end of line",
|
|
460
|
-
description:
|
|
474
|
+
description: options.keybindings.getDisplayString("tui.editor.cursorLineEnd"),
|
|
461
475
|
keywords: ["move", "cursor", "line", "end"],
|
|
462
476
|
execute: options.moveCursorToLineEnd,
|
|
463
477
|
},
|
|
464
478
|
];
|
|
465
479
|
|
|
466
|
-
return new PromptActionAutocompleteProvider(
|
|
480
|
+
return new PromptActionAutocompleteProvider(
|
|
481
|
+
options.commands,
|
|
482
|
+
options.basePath,
|
|
483
|
+
actions,
|
|
484
|
+
options.getPromptSuggestion,
|
|
485
|
+
);
|
|
467
486
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { logger } from "@gajae-code/utils";
|
|
2
|
+
import { generatePromptSuggestion } from "../utils/prompt-suggestion";
|
|
3
|
+
import type { InteractiveModeContext } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Owns the ghost-text next-prompt prediction lifecycle for interactive mode.
|
|
7
|
+
*
|
|
8
|
+
* After each agent turn ends (and only when `promptSuggestions` is enabled
|
|
9
|
+
* and the composer is empty), a smol-model prediction of the user's next
|
|
10
|
+
* prompt is generated in the background. The result renders as dim ghost
|
|
11
|
+
* text in the empty composer via the autocomplete provider's inline hint;
|
|
12
|
+
* Tab accepts it, typing dismisses it, and a new turn clears it.
|
|
13
|
+
*/
|
|
14
|
+
export class PromptSuggestionController {
|
|
15
|
+
#ctx: InteractiveModeContext;
|
|
16
|
+
#current: string | null = null;
|
|
17
|
+
/** Monotonic token; bumping it invalidates any in-flight generation. */
|
|
18
|
+
#generation = 0;
|
|
19
|
+
|
|
20
|
+
constructor(ctx: InteractiveModeContext) {
|
|
21
|
+
this.#ctx = ctx;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Current suggestion to render as ghost text, or null. */
|
|
25
|
+
get current(): string | null {
|
|
26
|
+
return this.#current;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Drop the current suggestion and invalidate any in-flight generation. */
|
|
30
|
+
clear(): void {
|
|
31
|
+
this.#generation++;
|
|
32
|
+
if (this.#current === null) return;
|
|
33
|
+
this.#current = null;
|
|
34
|
+
this.#ctx.ui.requestRender();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A new agent turn started; any pending suggestion is stale. */
|
|
38
|
+
onAgentStart(): void {
|
|
39
|
+
this.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Editor content changed. A non-empty composer means the user is writing
|
|
44
|
+
* their own prompt: dismiss the suggestion and cancel pending generation.
|
|
45
|
+
*/
|
|
46
|
+
notifyEditorChanged(text: string): void {
|
|
47
|
+
if (text.length > 0) this.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Agent turn ended; kick off background generation when the feature is
|
|
52
|
+
* enabled and the composer is idle and empty.
|
|
53
|
+
*/
|
|
54
|
+
onAgentEnd(): void {
|
|
55
|
+
if (!this.#ctx.settings.get("promptSuggestions")) return;
|
|
56
|
+
if (this.#ctx.shutdownRequested) return;
|
|
57
|
+
if (this.#ctx.session.isStreaming) return;
|
|
58
|
+
if (this.#ctx.editor.getText().trim() !== "") return;
|
|
59
|
+
|
|
60
|
+
const generation = ++this.#generation;
|
|
61
|
+
void generatePromptSuggestion(
|
|
62
|
+
this.#ctx.session.messages,
|
|
63
|
+
this.#ctx.session.modelRegistry,
|
|
64
|
+
this.#ctx.settings,
|
|
65
|
+
this.#ctx.session.sessionId,
|
|
66
|
+
this.#ctx.session.model,
|
|
67
|
+
provider => this.#ctx.session.agent.metadataForProvider(provider),
|
|
68
|
+
)
|
|
69
|
+
.then(suggestion => {
|
|
70
|
+
if (generation !== this.#generation) return;
|
|
71
|
+
if (!suggestion) return;
|
|
72
|
+
// The world may have moved on while the model was thinking.
|
|
73
|
+
if (this.#ctx.session.isStreaming) return;
|
|
74
|
+
if (this.#ctx.editor.getText().trim() !== "") return;
|
|
75
|
+
this.#current = suggestion;
|
|
76
|
+
this.#ctx.ui.requestRender();
|
|
77
|
+
})
|
|
78
|
+
.catch(error => {
|
|
79
|
+
logger.debug("prompt-suggestion: generation failed", { error: String(error) });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Tab handler for the composer: accept the suggestion when the composer
|
|
85
|
+
* is empty. Returns true when Tab was consumed.
|
|
86
|
+
*/
|
|
87
|
+
tryAcceptOnTab(text: string): boolean {
|
|
88
|
+
const suggestion = this.#current;
|
|
89
|
+
if (!suggestion || text.trim() !== "") return false;
|
|
90
|
+
// setText fires onChange with non-empty text, which clears #current.
|
|
91
|
+
this.#ctx.editor.setText(suggestion);
|
|
92
|
+
this.clear();
|
|
93
|
+
this.#ctx.ui.requestRender();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|