@gajae-code/coding-agent 0.9.0 → 0.9.2
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 +48 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +7 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +15 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/thinking-selector.d.ts +7 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/notifications/config-commands.d.ts +38 -0
- package/dist/types/notifications/config.d.ts +16 -0
- package/dist/types/notifications/index.d.ts +14 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +22 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +10 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +6 -3
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/package.json +8 -11
- package/src/cli/args.ts +8 -0
- package/src/cli/notify-cli.ts +38 -10
- package/src/cli.ts +5 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/notify.ts +2 -2
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +21 -4
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +11 -2
- package/src/config/settings.ts +24 -1
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/gjc-runtime/launch-tmux.ts +78 -12
- package/src/gjc-runtime/psmux-detect.ts +17 -2
- package/src/gjc-runtime/team-runtime.ts +46 -7
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +26 -3
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/thinking-selector.ts +83 -20
- package/src/modes/controllers/event-controller.ts +11 -2
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +62 -26
- package/src/modes/controllers/selector-controller.ts +43 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/modes/rpc/rpc-mode.ts +3 -9
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +48 -15
- package/src/notifications/config-commands.ts +90 -0
- package/src/notifications/config.ts +27 -0
- package/src/notifications/index.ts +184 -14
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +143 -0
- package/src/notifications/telegram-daemon-cli.ts +60 -5
- package/src/notifications/telegram-daemon.ts +431 -91
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +42 -1
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/runtime-credential-selector.ts +35 -0
- package/src/sdk.ts +104 -13
- package/src/session/agent-session.ts +48 -22
- package/src/slash-commands/builtin-registry.ts +78 -1
- package/src/system-prompt.ts +4 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +15 -2
- package/src/utils/pasted-image-path.ts +170 -0
package/src/lsp/client.ts
CHANGED
|
@@ -835,6 +835,7 @@ async function shutdownClientInstance(client: LspClient): Promise<void> {
|
|
|
835
835
|
await Promise.race([shutdown, Bun.sleep(5_000)]);
|
|
836
836
|
await client.owner?.dispose();
|
|
837
837
|
await client.owner?.awaitExit({ timeoutMs: 1_000 });
|
|
838
|
+
await Promise.race([client.proc.exited.catch(() => undefined), Bun.sleep(1_000)]);
|
|
838
839
|
}
|
|
839
840
|
|
|
840
841
|
export async function shutdownClient(key: string): Promise<void> {
|
package/src/main.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type { InteractiveMode } from "./modes/interactive-mode";
|
|
|
38
38
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
39
39
|
import type { SubmittedUserInput } from "./modes/types";
|
|
40
40
|
import { applyCliRuntimeApiKeyOverride } from "./runtime-api-key";
|
|
41
|
+
import { parseCliCredentialSelector } from "./runtime-credential-selector";
|
|
41
42
|
import type { MCPManager } from "./runtime-mcp";
|
|
42
43
|
import {
|
|
43
44
|
type CreateAgentSessionOptions,
|
|
@@ -217,10 +218,14 @@ export async function applyStartupModelProfiles(args: {
|
|
|
217
218
|
startupModel?: CreateAgentSessionOptions["model"];
|
|
218
219
|
startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
219
220
|
}): Promise<void> {
|
|
220
|
-
const applyProfile = async (
|
|
221
|
+
const applyProfile = async (
|
|
222
|
+
profileName: string,
|
|
223
|
+
persistDefault: boolean,
|
|
224
|
+
options: { thinkingLevelOverride?: CreateAgentSessionOptions["thinkingLevel"] } = {},
|
|
225
|
+
): Promise<void> => {
|
|
221
226
|
await activateModelProfile(
|
|
222
227
|
{ session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
|
|
223
|
-
{ persistDefault },
|
|
228
|
+
{ persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
|
|
224
229
|
);
|
|
225
230
|
};
|
|
226
231
|
|
|
@@ -234,7 +239,11 @@ export async function applyStartupModelProfiles(args: {
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
if (defaultProfile) {
|
|
237
|
-
await applyProfile(defaultProfile, false
|
|
242
|
+
await applyProfile(defaultProfile, false, {
|
|
243
|
+
thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
|
|
244
|
+
? args.settings.get("defaultThinkingLevel")
|
|
245
|
+
: undefined,
|
|
246
|
+
});
|
|
238
247
|
}
|
|
239
248
|
if (args.parsedArgs.mpreset) {
|
|
240
249
|
await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true);
|
|
@@ -1002,6 +1011,20 @@ export async function runRootCommand(
|
|
|
1002
1011
|
deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
|
|
1003
1012
|
|
|
1004
1013
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
1014
|
+
if (parsedArgs.apiKey && parsedArgs.credential) {
|
|
1015
|
+
process.stderr.write(`${chalk.red("--api-key and --credential cannot be used together")}\n`);
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (parsedArgs.credential) {
|
|
1020
|
+
try {
|
|
1021
|
+
sessionOptions.credentialSelector = parseCliCredentialSelector(parsedArgs.credential);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1024
|
+
process.stderr.write(`${chalk.red(message)}\n`);
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1005
1028
|
if (parsedArgs.apiKey) {
|
|
1006
1029
|
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
1007
1030
|
process.stderr.write(
|
|
@@ -16,6 +16,7 @@ import { resolveCurrentBranch } from "./status-line/git-utils";
|
|
|
16
16
|
export class FooterComponent implements Component {
|
|
17
17
|
#cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
|
|
18
18
|
#gitWatcher: fs.FSWatcher | null = null;
|
|
19
|
+
#gitWatcherGeneration = 0;
|
|
19
20
|
#onBranchChange: (() => void) | null = null;
|
|
20
21
|
#autoCompactEnabled: boolean = true;
|
|
21
22
|
#extensionStatuses: Map<string, string> = new Map();
|
|
@@ -51,7 +52,10 @@ export class FooterComponent implements Component {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
#setupGitWatcher(): void {
|
|
54
|
-
// Clean up existing watcher
|
|
55
|
+
// Clean up existing watcher. The generation guard below keeps a stale
|
|
56
|
+
// in-flight resolve — one that dispose() or a newer setup already
|
|
57
|
+
// superseded — from installing a watcher nothing would ever close.
|
|
58
|
+
const generation = ++this.#gitWatcherGeneration;
|
|
55
59
|
if (this.#gitWatcher) {
|
|
56
60
|
this.#gitWatcher.close();
|
|
57
61
|
this.#gitWatcher = null;
|
|
@@ -60,7 +64,7 @@ export class FooterComponent implements Component {
|
|
|
60
64
|
void git.head
|
|
61
65
|
.resolve(getProjectDir())
|
|
62
66
|
.then(head => {
|
|
63
|
-
if (!head) {
|
|
67
|
+
if (!head || generation !== this.#gitWatcherGeneration) {
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -84,6 +88,7 @@ export class FooterComponent implements Component {
|
|
|
84
88
|
* Clean up the file watcher
|
|
85
89
|
*/
|
|
86
90
|
dispose(): void {
|
|
91
|
+
this.#gitWatcherGeneration++;
|
|
87
92
|
if (this.#gitWatcher) {
|
|
88
93
|
this.#gitWatcher.close();
|
|
89
94
|
this.#gitWatcher = null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Effort } from "@gajae-code/ai";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
@@ -654,7 +654,7 @@ export interface SettingsRuntimeContext {
|
|
|
654
654
|
/** Available thinking levels (from session) */
|
|
655
655
|
availableThinkingLevels: Effort[];
|
|
656
656
|
/** Current thinking level (from session) */
|
|
657
|
-
thinkingLevel:
|
|
657
|
+
thinkingLevel: ThinkingLevelValue | undefined;
|
|
658
658
|
/** Available themes */
|
|
659
659
|
availableThemes: string[];
|
|
660
660
|
/** Available model profile names (from the model registry) */
|
|
@@ -843,7 +843,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
843
843
|
|
|
844
844
|
// Special case: inject runtime options for thinking level
|
|
845
845
|
if (def.path === "defaultThinkingLevel") {
|
|
846
|
-
options = this.context.availableThinkingLevels.map(level => {
|
|
846
|
+
options = [ThinkingLevel.Off, ...this.context.availableThinkingLevels].map(level => {
|
|
847
847
|
const baseOpt = options.find(o => o.value === level);
|
|
848
848
|
return baseOpt || { value: level, label: level };
|
|
849
849
|
});
|
|
@@ -1,51 +1,114 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { Container, type SelectItem, SelectList } from "@gajae-code/tui";
|
|
3
|
-
import { getSelectListTheme } from "../../modes/theme/theme";
|
|
4
|
-
import { getThinkingLevelMetadata } from "../../thinking";
|
|
1
|
+
import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@gajae-code/agent-core";
|
|
2
|
+
import { Container, type SelectItem, SelectList, Text } from "@gajae-code/tui";
|
|
3
|
+
import { getSelectListTheme, theme } from "../../modes/theme/theme";
|
|
4
|
+
import { getThinkingLevelMetadata, type ThinkingLevelValue as ThinkingMetadataValue } from "../../thinking-metadata";
|
|
5
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
6
6
|
|
|
7
|
+
const SCOPE_ITEMS = [
|
|
8
|
+
{
|
|
9
|
+
value: "session",
|
|
10
|
+
label: "Apply for this session",
|
|
11
|
+
description: "Use this effort until the session changes it again",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
value: "default",
|
|
15
|
+
label: "Set as default",
|
|
16
|
+
description: "Save this effort for future sessions",
|
|
17
|
+
},
|
|
18
|
+
] as const satisfies readonly SelectItem[];
|
|
19
|
+
|
|
20
|
+
export interface ThinkingSelectorSelection {
|
|
21
|
+
level: ThinkingLevelValue;
|
|
22
|
+
persistDefault: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
/**
|
|
8
26
|
* Component that renders a thinking level selector with borders
|
|
9
27
|
*/
|
|
10
28
|
export class ThinkingSelectorComponent extends Container {
|
|
11
29
|
#selectList: SelectList;
|
|
30
|
+
#levelList: SelectList;
|
|
31
|
+
#selectedLevel: ThinkingLevelValue | undefined;
|
|
32
|
+
readonly #onSelect: (selection: ThinkingSelectorSelection) => void;
|
|
33
|
+
readonly #onCancel: () => void;
|
|
12
34
|
|
|
13
35
|
constructor(
|
|
14
|
-
currentLevel:
|
|
15
|
-
availableLevels:
|
|
16
|
-
onSelect: (
|
|
36
|
+
currentLevel: ThinkingLevelValue | undefined,
|
|
37
|
+
availableLevels: ThinkingLevelValue[],
|
|
38
|
+
onSelect: (selection: ThinkingSelectorSelection) => void,
|
|
17
39
|
onCancel: () => void,
|
|
18
40
|
) {
|
|
19
41
|
super();
|
|
20
42
|
|
|
21
|
-
|
|
43
|
+
this.#onSelect = onSelect;
|
|
44
|
+
this.#onCancel = onCancel;
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
|
|
46
|
+
const currentValue = currentLevel ?? ThinkingLevel.Off;
|
|
47
|
+
const thinkingLevels: SelectItem[] = availableLevels.map(level => {
|
|
48
|
+
const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
|
|
49
|
+
return level === currentValue
|
|
50
|
+
? { ...metadata, label: metadata.label + theme.fg("muted", " (current)") }
|
|
51
|
+
: metadata;
|
|
52
|
+
});
|
|
25
53
|
|
|
26
|
-
|
|
27
|
-
this.#selectList =
|
|
54
|
+
this.#levelList = new SelectList(thinkingLevels, thinkingLevels.length, getSelectListTheme());
|
|
55
|
+
this.#selectList = this.#levelList;
|
|
28
56
|
|
|
29
57
|
// Preselect current level
|
|
30
|
-
const currentIndex = thinkingLevels.findIndex(item => item.value ===
|
|
58
|
+
const currentIndex = thinkingLevels.findIndex(item => item.value === currentValue);
|
|
31
59
|
if (currentIndex !== -1) {
|
|
32
|
-
this.#
|
|
60
|
+
this.#levelList.setSelectedIndex(currentIndex);
|
|
33
61
|
}
|
|
34
62
|
|
|
35
|
-
this.#
|
|
36
|
-
|
|
63
|
+
this.#levelList.onSelect = item => {
|
|
64
|
+
this.#selectedLevel = item.value as ThinkingLevelValue;
|
|
65
|
+
this.#renderScopeList();
|
|
37
66
|
};
|
|
38
67
|
|
|
39
|
-
this.#
|
|
40
|
-
onCancel();
|
|
68
|
+
this.#levelList.onCancel = () => {
|
|
69
|
+
this.#onCancel();
|
|
41
70
|
};
|
|
42
71
|
|
|
43
|
-
this
|
|
72
|
+
this.#renderLevelList();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#renderLevelList(): void {
|
|
76
|
+
this.detachAll();
|
|
77
|
+
this.#selectedLevel = undefined;
|
|
78
|
+
this.#selectList = this.#levelList;
|
|
79
|
+
this.addChild(new DynamicBorder());
|
|
80
|
+
this.addChild(this.#levelList);
|
|
81
|
+
this.addChild(new DynamicBorder());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#renderScopeList(): void {
|
|
85
|
+
const level = this.#selectedLevel;
|
|
86
|
+
if (!level) return;
|
|
87
|
+
|
|
88
|
+
const scopeList = new SelectList(SCOPE_ITEMS, SCOPE_ITEMS.length, getSelectListTheme());
|
|
89
|
+
scopeList.onSelect = item => {
|
|
90
|
+
this.#onSelect({
|
|
91
|
+
level,
|
|
92
|
+
persistDefault: item.value === "default",
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
scopeList.onCancel = () => {
|
|
96
|
+
this.#renderLevelList();
|
|
97
|
+
};
|
|
44
98
|
|
|
45
|
-
|
|
99
|
+
const metadata = getThinkingLevelMetadata(level as ThinkingMetadataValue);
|
|
100
|
+
this.detachAll();
|
|
101
|
+
this.#selectList = scopeList;
|
|
102
|
+
this.addChild(new DynamicBorder());
|
|
103
|
+
this.addChild(new Text(theme.fg("muted", ` Reasoning effort: ${metadata.label}`), 0, 0));
|
|
104
|
+
this.addChild(scopeList);
|
|
46
105
|
this.addChild(new DynamicBorder());
|
|
47
106
|
}
|
|
48
107
|
|
|
108
|
+
handleInput(data: string): void {
|
|
109
|
+
this.#selectList.handleInput(data);
|
|
110
|
+
}
|
|
111
|
+
|
|
49
112
|
getSelectList(): SelectList {
|
|
50
113
|
return this.#selectList;
|
|
51
114
|
}
|
|
@@ -16,6 +16,7 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
|
16
16
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
17
17
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
18
18
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
19
|
+
import { completionNotifyDisabledByEnv } from "../../notifications/config";
|
|
19
20
|
import { summaryFromMessage } from "../../notifications/helpers";
|
|
20
21
|
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
21
22
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
@@ -23,6 +24,7 @@ import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
|
|
23
24
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
24
25
|
import { interruptHint } from "../shared";
|
|
25
26
|
import { buildAbortDisplayMessage } from "../utils/abort-message";
|
|
27
|
+
import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
|
|
26
28
|
import { ringTerminalBell } from "../utils/terminal-bell";
|
|
27
29
|
import { argsWithPartialJson } from "../utils/ui-helpers";
|
|
28
30
|
|
|
@@ -291,12 +293,17 @@ export class EventController {
|
|
|
291
293
|
const signature = `${textContent}\u0000${imageCount}`;
|
|
292
294
|
|
|
293
295
|
this.#resetReadGroup();
|
|
294
|
-
const
|
|
296
|
+
const wasSingleOptimistic = this.ctx.optimisticUserMessageSignature === signature;
|
|
297
|
+
const wasInjectedOptimistic = !wasSingleOptimistic && consumeInjectedOptimisticSignature(this.ctx, signature);
|
|
298
|
+
const wasOptimistic = wasSingleOptimistic || wasInjectedOptimistic;
|
|
295
299
|
const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
|
|
296
300
|
if (!wasOptimistic) {
|
|
297
301
|
this.ctx.addMessageToChat(event.message);
|
|
298
302
|
}
|
|
299
|
-
|
|
303
|
+
// Only clear the single local slot when IT matched; an injected match is
|
|
304
|
+
// consumed from the counting Map above and must not clobber a coexisting
|
|
305
|
+
// pending local optimistic signature.
|
|
306
|
+
if (wasSingleOptimistic) {
|
|
300
307
|
this.ctx.optimisticUserMessageSignature = undefined;
|
|
301
308
|
}
|
|
302
309
|
|
|
@@ -960,6 +967,8 @@ export class EventController {
|
|
|
960
967
|
}
|
|
961
968
|
|
|
962
969
|
sendCompletionNotification(): void {
|
|
970
|
+
// Per-run hard opt-out (env, config-untouched, child-inheritable): GJC_NOTIFY=off.
|
|
971
|
+
if (completionNotifyDisabledByEnv(process.env)) return;
|
|
963
972
|
const isBackgrounded = this.ctx.isBackgrounded !== false;
|
|
964
973
|
const notify = settings.get("completion.notify");
|
|
965
974
|
if (notify === "off") return;
|
|
@@ -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 { applyInjectedUserSubmission } from "../utils/injected-user-submission";
|
|
26
27
|
import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
|
|
27
28
|
|
|
28
29
|
const MAX_WIDGET_LINES = 10;
|
|
@@ -954,7 +955,13 @@ export class ExtensionUiController {
|
|
|
954
955
|
}
|
|
955
956
|
|
|
956
957
|
#sendExtensionUserMessage: SendUserMessageHandler = (content, options) => {
|
|
957
|
-
|
|
958
|
+
// Compute queued BEFORE send: prompt() may flip session.isStreaming synchronously.
|
|
959
|
+
const queued = Boolean(options?.deliverAs) || this.ctx.session.isStreaming;
|
|
960
|
+
// Call send first so the busy/queued path finds the session queue populated
|
|
961
|
+
// (queueSteer/queueFollowUp push synchronously) before refreshing pending display.
|
|
962
|
+
const send = this.ctx.session.sendUserMessage(content, options);
|
|
963
|
+
applyInjectedUserSubmission(this.ctx, { content, queued });
|
|
964
|
+
send.catch((err: unknown) => {
|
|
958
965
|
this.ctx.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
959
966
|
});
|
|
960
967
|
};
|
|
@@ -18,6 +18,7 @@ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
|
|
|
18
18
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
19
19
|
import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
|
|
20
20
|
import { resizeImage } from "../../utils/image-resize";
|
|
21
|
+
import { resolvePastedImagePath } from "../../utils/pasted-image-path";
|
|
21
22
|
import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
|
|
22
23
|
import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
|
|
23
24
|
|
|
@@ -26,8 +27,8 @@ interface Expandable {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const IMAGE_PLACEHOLDER_PATTERN = /\[image ([1-9]\d*)\]/g;
|
|
31
|
+
const IMAGE_PLACEHOLDER_PRESENT_PATTERN = /\[image [1-9]\d*\]/;
|
|
31
32
|
|
|
32
33
|
function isExpandable(obj: unknown): obj is Expandable {
|
|
33
34
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
@@ -384,6 +385,7 @@ export class InputController {
|
|
|
384
385
|
this.ctx.isBashMode = trimmed.startsWith("!");
|
|
385
386
|
this.ctx.isBashNoContext = trimmed.startsWith("!!");
|
|
386
387
|
this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
|
|
388
|
+
this.#clearPendingImagesIfPlaceholdersRemoved(text);
|
|
387
389
|
if (
|
|
388
390
|
wasBashMode !== this.ctx.isBashMode ||
|
|
389
391
|
wasBashNoContext !== this.ctx.isBashNoContext ||
|
|
@@ -419,7 +421,7 @@ export class InputController {
|
|
|
419
421
|
}
|
|
420
422
|
|
|
421
423
|
const runner = this.ctx.session.extensionRunner;
|
|
422
|
-
let inputImages = this
|
|
424
|
+
let inputImages = this.#visiblePendingImagesForText(text);
|
|
423
425
|
|
|
424
426
|
if (runner?.hasHandlers("input")) {
|
|
425
427
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
@@ -499,7 +501,7 @@ export class InputController {
|
|
|
499
501
|
|
|
500
502
|
// Queue input during compaction
|
|
501
503
|
if (this.ctx.session.isCompacting) {
|
|
502
|
-
if (
|
|
504
|
+
if ((inputImages?.length ?? 0) > 0) {
|
|
503
505
|
this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
|
|
504
506
|
return;
|
|
505
507
|
}
|
|
@@ -521,11 +523,13 @@ export class InputController {
|
|
|
521
523
|
// typed since queuing intact. Same protection as #783, applied to
|
|
522
524
|
// the streaming/queue path.
|
|
523
525
|
const streamingBehavior = this.#busyStreamingBehavior();
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
)
|
|
526
|
+
const promptOptions =
|
|
527
|
+
streamingBehavior === "followUp"
|
|
528
|
+
? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
|
|
529
|
+
: { streamingBehavior, images };
|
|
530
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
|
|
531
|
+
imageCount: images?.length ?? 0,
|
|
532
|
+
});
|
|
529
533
|
this.ctx.updatePendingMessagesDisplay();
|
|
530
534
|
this.ctx.ui.requestRender();
|
|
531
535
|
return;
|
|
@@ -870,7 +874,9 @@ export class InputController {
|
|
|
870
874
|
details,
|
|
871
875
|
attribution: "user",
|
|
872
876
|
},
|
|
873
|
-
|
|
877
|
+
streamingBehavior === "followUp"
|
|
878
|
+
? { streamingBehavior, followUpQueuePolicy: "sequential" }
|
|
879
|
+
: { streamingBehavior },
|
|
874
880
|
);
|
|
875
881
|
}
|
|
876
882
|
if (this.ctx.session.isStreaming) {
|
|
@@ -912,7 +918,10 @@ export class InputController {
|
|
|
912
918
|
this.ctx.editor.addToHistory(text);
|
|
913
919
|
this.ctx.editor.setText("");
|
|
914
920
|
await this.ctx.withLocalSubmission(text, () =>
|
|
915
|
-
this.ctx.session.prompt(text, {
|
|
921
|
+
this.ctx.session.prompt(text, {
|
|
922
|
+
streamingBehavior: "followUp",
|
|
923
|
+
followUpQueuePolicy: "sequential",
|
|
924
|
+
}),
|
|
916
925
|
);
|
|
917
926
|
this.ctx.updatePendingMessagesDisplay();
|
|
918
927
|
this.ctx.ui.requestRender();
|
|
@@ -1061,10 +1070,15 @@ export class InputController {
|
|
|
1061
1070
|
}
|
|
1062
1071
|
|
|
1063
1072
|
handleTextPaste(text: string): boolean | Promise<boolean> {
|
|
1064
|
-
const imagePath = this
|
|
1073
|
+
const imagePath = resolvePastedImagePath(text, { cwd: this.ctx.sessionManager.getCwd() });
|
|
1065
1074
|
return imagePath ? this.#attachPastedImagePath(imagePath) : false;
|
|
1066
1075
|
}
|
|
1067
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* Returns `false` on every failure path so the editor replays the original
|
|
1079
|
+
* bracketed paste — the raw path text must never be lost when attachment
|
|
1080
|
+
* is impossible (unsupported content, oversized image, load error).
|
|
1081
|
+
*/
|
|
1068
1082
|
async #attachPastedImagePath(imagePath: string): Promise<boolean> {
|
|
1069
1083
|
try {
|
|
1070
1084
|
const image = await loadImageInput({
|
|
@@ -1073,8 +1087,8 @@ export class InputController {
|
|
|
1073
1087
|
autoResize: this.ctx.settings.get("images.autoResize"),
|
|
1074
1088
|
});
|
|
1075
1089
|
if (!image) {
|
|
1076
|
-
this.ctx.showStatus("Unsupported pasted
|
|
1077
|
-
return
|
|
1090
|
+
this.ctx.showStatus("Unsupported pasted image file");
|
|
1091
|
+
return false;
|
|
1078
1092
|
}
|
|
1079
1093
|
|
|
1080
1094
|
this.ctx.pendingImages.push({
|
|
@@ -1089,26 +1103,48 @@ export class InputController {
|
|
|
1089
1103
|
} catch (error) {
|
|
1090
1104
|
if (error instanceof ImageInputTooLargeError) {
|
|
1091
1105
|
this.ctx.showStatus(error.message);
|
|
1092
|
-
return
|
|
1106
|
+
return false;
|
|
1093
1107
|
}
|
|
1094
|
-
this.ctx.showStatus("Failed to attach pasted
|
|
1095
|
-
return
|
|
1108
|
+
this.ctx.showStatus("Failed to attach pasted image");
|
|
1109
|
+
return false;
|
|
1096
1110
|
}
|
|
1097
1111
|
}
|
|
1098
1112
|
|
|
1099
|
-
#getPastedImagePathCandidate(text: string): string | undefined {
|
|
1100
|
-
const resolvedPath = path.resolve(text.trim());
|
|
1101
|
-
const parentDir = path.dirname(resolvedPath);
|
|
1102
|
-
const isClipboardTempPath =
|
|
1103
|
-
(parentDir === "/tmp" || MACOS_CLIPBOARD_TEMP_DIR_PATTERN.test(parentDir)) &&
|
|
1104
|
-
CLIPBOARD_TEMP_IMAGE_FILE_PATTERN.test(path.basename(resolvedPath));
|
|
1105
|
-
return isClipboardTempPath ? resolvedPath : undefined;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
1113
|
#nextImagePlaceholder(): string {
|
|
1109
1114
|
return `[image ${this.ctx.pendingImages.length}]`;
|
|
1110
1115
|
}
|
|
1111
1116
|
|
|
1117
|
+
#visiblePendingImagesForText(text: string): InteractiveModeContext["pendingImages"] | undefined {
|
|
1118
|
+
if (this.ctx.pendingImages.length === 0) {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const images: InteractiveModeContext["pendingImages"] = [];
|
|
1123
|
+
const seenImageIndexes = new Set<number>();
|
|
1124
|
+
for (const match of text.matchAll(IMAGE_PLACEHOLDER_PATTERN)) {
|
|
1125
|
+
const placeholderNumberText = match[1];
|
|
1126
|
+
if (!placeholderNumberText) continue;
|
|
1127
|
+
const placeholderNumber = Number.parseInt(placeholderNumberText, 10);
|
|
1128
|
+
const imageIndex = placeholderNumber - 1;
|
|
1129
|
+
if (imageIndex < 0 || imageIndex >= this.ctx.pendingImages.length || seenImageIndexes.has(imageIndex)) {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const image = this.ctx.pendingImages[imageIndex];
|
|
1133
|
+
if (!image) continue;
|
|
1134
|
+
images.push(image);
|
|
1135
|
+
seenImageIndexes.add(imageIndex);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return images.length > 0 ? images : undefined;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
#clearPendingImagesIfPlaceholdersRemoved(text: string): void {
|
|
1142
|
+
if (this.ctx.pendingImages.length === 0 || IMAGE_PLACEHOLDER_PRESENT_PATTERN.test(text)) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
this.ctx.pendingImages = [];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1112
1148
|
async handleImagePaste(): Promise<boolean> {
|
|
1113
1149
|
try {
|
|
1114
1150
|
const image = await readImageFromClipboard();
|
|
@@ -88,6 +88,7 @@ import { SessionSelectorComponent } from "../components/session-selector";
|
|
|
88
88
|
import { SettingsSelectorComponent } from "../components/settings-selector";
|
|
89
89
|
import type { StatusLineSettings } from "../components/status-line";
|
|
90
90
|
import { ThemeSelectorComponent } from "../components/theme-selector";
|
|
91
|
+
import { ThinkingSelectorComponent } from "../components/thinking-selector";
|
|
91
92
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
92
93
|
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
93
94
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
@@ -431,6 +432,48 @@ export class SelectorController {
|
|
|
431
432
|
});
|
|
432
433
|
}
|
|
433
434
|
|
|
435
|
+
showEffortSelector(): void {
|
|
436
|
+
const availableLevels = [
|
|
437
|
+
ThinkingLevel.Inherit,
|
|
438
|
+
ThinkingLevel.Off,
|
|
439
|
+
...this.ctx.session.getAvailableThinkingLevels(),
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
this.showSelector(done => {
|
|
443
|
+
const selector = new ThinkingSelectorComponent(
|
|
444
|
+
this.ctx.session.thinkingLevel,
|
|
445
|
+
availableLevels,
|
|
446
|
+
selection => {
|
|
447
|
+
done();
|
|
448
|
+
|
|
449
|
+
const { level, persistDefault } = selection;
|
|
450
|
+
const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
|
|
451
|
+
const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
|
|
452
|
+
this.ctx.session.setThinkingLevel(levelToApply, persistDefault);
|
|
453
|
+
const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
|
|
454
|
+
const requestedLabel =
|
|
455
|
+
level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
|
|
456
|
+
const clampedSuffix =
|
|
457
|
+
effectiveLevel === levelToApply ? "" : ` Requested ${levelToApply}; effective ${effectiveLevel}.`;
|
|
458
|
+
|
|
459
|
+
this.ctx.statusLine.invalidate();
|
|
460
|
+
this.ctx.updateEditorBorderColor();
|
|
461
|
+
this.ctx.updateEditorTopBorder();
|
|
462
|
+
if (persistDefault) void this.ctx.notifyConfigChanged?.();
|
|
463
|
+
this.ctx.ui.requestRender();
|
|
464
|
+
const scopeLabel = persistDefault ? "Default reasoning effort" : "Reasoning effort";
|
|
465
|
+
this.ctx.showStatus(
|
|
466
|
+
`${scopeLabel} set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
|
|
467
|
+
);
|
|
468
|
+
},
|
|
469
|
+
() => {
|
|
470
|
+
done();
|
|
471
|
+
this.ctx.ui.requestRender();
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
return { component: selector, focus: selector };
|
|
475
|
+
});
|
|
476
|
+
}
|
|
434
477
|
showSettingsSelector(): void {
|
|
435
478
|
getAvailableThemes().then(availableThemes => {
|
|
436
479
|
this.showSelector(done => {
|
|
@@ -330,6 +330,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
330
330
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
331
331
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
332
332
|
locallySubmittedUserSignatures: Set<string> = new Set();
|
|
333
|
+
optimisticInjectedSignatures: Map<string, number> = new Map();
|
|
333
334
|
#pendingSubmittedInput: SubmittedUserInput | undefined;
|
|
334
335
|
#pendingSubmissionDispose: (() => void) | undefined;
|
|
335
336
|
lastSigintTime = 0;
|
|
@@ -2560,6 +2561,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2560
2561
|
this.#selectorController.showModelSelector(options);
|
|
2561
2562
|
}
|
|
2562
2563
|
|
|
2564
|
+
showEffortSelector(): void {
|
|
2565
|
+
this.#selectorController.showEffortSelector();
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2563
2568
|
showProviderOnboarding(): void {
|
|
2564
2569
|
this.#selectorController.showProviderOnboarding();
|
|
2565
2570
|
}
|
|
@@ -173,15 +173,9 @@ function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "e
|
|
|
173
173
|
return "info";
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`RPC --listen refused: a live server is already listening on ${socketPath}. ` +
|
|
180
|
-
"Stop it first or choose a different --listen path.",
|
|
181
|
-
);
|
|
182
|
-
this.name = "RpcListenRefusedError";
|
|
183
|
-
}
|
|
184
|
-
}
|
|
176
|
+
// Thrown by prepareRpcSocketPath when the --listen path has a live owner;
|
|
177
|
+
// re-exported here because main.ts catches it at the RPC launch boundary.
|
|
178
|
+
export { RpcListenRefusedError } from "./rpc-socket-security";
|
|
185
179
|
|
|
186
180
|
/**
|
|
187
181
|
* Probe whether a unix-domain socket path has a live server accepting
|
|
@@ -9,6 +9,18 @@ export class RpcSocketSecurityError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// The launch boundary in main.ts catches exactly this class to turn a
|
|
13
|
+
// duplicate --listen into a clean stderr diagnostic + non-zero exit (issue 19).
|
|
14
|
+
export class RpcListenRefusedError extends Error {
|
|
15
|
+
constructor(socketPath: string) {
|
|
16
|
+
super(
|
|
17
|
+
`RPC --listen refused: a live server is already listening on ${socketPath}. ` +
|
|
18
|
+
"Stop it first or choose a different --listen path.",
|
|
19
|
+
);
|
|
20
|
+
this.name = "RpcListenRefusedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
const unsafeBits = 0o077;
|
|
13
25
|
|
|
14
26
|
function currentUid(): number | undefined {
|
|
@@ -53,7 +65,7 @@ export async function prepareRpcSocketPath(socketPath: string): Promise<void> {
|
|
|
53
65
|
if (!existing.isSocket()) throw new RpcSocketSecurityError(`RPC socket path is not a socket: ${socketPath}`);
|
|
54
66
|
assertPrivateMode(existing.mode, socketPath);
|
|
55
67
|
if (await probeUnixSocketAlive(socketPath)) {
|
|
56
|
-
throw new
|
|
68
|
+
throw new RpcListenRefusedError(socketPath);
|
|
57
69
|
}
|
|
58
70
|
await fs.unlink(socketPath);
|
|
59
71
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { Theme } from "./theme/theme";
|
|
|
34
34
|
export type CompactionQueuedMessage = {
|
|
35
35
|
text: string;
|
|
36
36
|
mode: "steer" | "followUp";
|
|
37
|
+
followUpQueuePolicy?: "sequential";
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export type SubmittedUserInput = {
|
|
@@ -116,6 +117,7 @@ export interface InteractiveModeContext {
|
|
|
116
117
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
117
118
|
optimisticUserMessageSignature: string | undefined;
|
|
118
119
|
locallySubmittedUserSignatures: Set<string>;
|
|
120
|
+
optimisticInjectedSignatures: Map<string, number>;
|
|
119
121
|
lastSigintTime: number;
|
|
120
122
|
lastEscapeTime: number;
|
|
121
123
|
lastComposerClearEscapeTime: number;
|
|
@@ -244,6 +246,7 @@ export interface InteractiveModeContext {
|
|
|
244
246
|
showExtensionsDashboard(): void;
|
|
245
247
|
showAgentsDashboard(): void;
|
|
246
248
|
showModelSelector(options?: { temporaryOnly?: boolean }): void;
|
|
249
|
+
showEffortSelector(): void;
|
|
247
250
|
showProviderOnboarding(): void;
|
|
248
251
|
showPluginSelector(mode?: "install" | "uninstall"): void;
|
|
249
252
|
showUserMessageSelector(): void;
|