@sayknow-cli/coding-agent 0.3.8 → 0.3.10
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 +13 -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 +11 -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/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.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/skc-runtime/tmux-common.d.ts +1 -0
- 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 +7 -7
- 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 +107 -11
- 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/internal-urls/docs-index.generated.ts +4 -4
- 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.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/skc-runtime/psmux-detect.ts +17 -2
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-runtime.ts +10 -1
- 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
|
@@ -39,6 +39,7 @@ import type { InteractiveMode } from "./modes/interactive-mode";
|
|
|
39
39
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
40
40
|
import type { SubmittedUserInput } from "./modes/types";
|
|
41
41
|
import { applyCliRuntimeApiKeyOverride } from "./runtime-api-key";
|
|
42
|
+
import { parseCliCredentialSelector } from "./runtime-credential-selector";
|
|
42
43
|
import type { MCPManager } from "./runtime-mcp";
|
|
43
44
|
import {
|
|
44
45
|
type CreateAgentSessionOptions,
|
|
@@ -218,10 +219,14 @@ export async function applyStartupModelProfiles(args: {
|
|
|
218
219
|
startupModel?: CreateAgentSessionOptions["model"];
|
|
219
220
|
startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
220
221
|
}): Promise<void> {
|
|
221
|
-
const applyProfile = async (
|
|
222
|
+
const applyProfile = async (
|
|
223
|
+
profileName: string,
|
|
224
|
+
persistDefault: boolean,
|
|
225
|
+
options: { thinkingLevelOverride?: CreateAgentSessionOptions["thinkingLevel"] } = {},
|
|
226
|
+
): Promise<void> => {
|
|
222
227
|
await activateModelProfile(
|
|
223
228
|
{ session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
|
|
224
|
-
{ persistDefault },
|
|
229
|
+
{ persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
|
|
225
230
|
);
|
|
226
231
|
};
|
|
227
232
|
|
|
@@ -235,7 +240,11 @@ export async function applyStartupModelProfiles(args: {
|
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
if (defaultProfile) {
|
|
238
|
-
await applyProfile(defaultProfile, false
|
|
243
|
+
await applyProfile(defaultProfile, false, {
|
|
244
|
+
thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
|
|
245
|
+
? args.settings.get("defaultThinkingLevel")
|
|
246
|
+
: undefined,
|
|
247
|
+
});
|
|
239
248
|
}
|
|
240
249
|
if (args.parsedArgs.mpreset) {
|
|
241
250
|
await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true);
|
|
@@ -1005,6 +1014,20 @@ export async function runRootCommand(
|
|
|
1005
1014
|
deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
|
|
1006
1015
|
|
|
1007
1016
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
1017
|
+
if (parsedArgs.apiKey && parsedArgs.credential) {
|
|
1018
|
+
process.stderr.write(`${chalk.red("--api-key and --credential cannot be used together")}\n`);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (parsedArgs.credential) {
|
|
1023
|
+
try {
|
|
1024
|
+
sessionOptions.credentialSelector = parseCliCredentialSelector(parsedArgs.credential);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1027
|
+
process.stderr.write(`${chalk.red(message)}\n`);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1008
1031
|
if (parsedArgs.apiKey) {
|
|
1009
1032
|
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
1010
1033
|
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 "@sayknow-cli/agent-core";
|
|
2
2
|
import type { Effort } from "@sayknow-cli/ai";
|
|
3
3
|
import {
|
|
4
4
|
Container,
|
|
@@ -666,7 +666,7 @@ export interface SettingsRuntimeContext {
|
|
|
666
666
|
/** Available thinking levels (from session) */
|
|
667
667
|
availableThinkingLevels: Effort[];
|
|
668
668
|
/** Current thinking level (from session) */
|
|
669
|
-
thinkingLevel:
|
|
669
|
+
thinkingLevel: ThinkingLevelValue | undefined;
|
|
670
670
|
/** Available themes */
|
|
671
671
|
availableThemes: string[];
|
|
672
672
|
/** Available model profile names (from the model registry) */
|
|
@@ -857,7 +857,7 @@ export class SettingsSelectorComponent extends Container {
|
|
|
857
857
|
|
|
858
858
|
// Special case: inject runtime options for thinking level
|
|
859
859
|
if (def.path === "defaultThinkingLevel") {
|
|
860
|
-
options = this.context.availableThinkingLevels.map(level => {
|
|
860
|
+
options = [ThinkingLevel.Off, ...this.context.availableThinkingLevels].map(level => {
|
|
861
861
|
const baseOpt = options.find(o => o.value === level);
|
|
862
862
|
return baseOpt || { value: level, label: level };
|
|
863
863
|
});
|
|
@@ -1,51 +1,114 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { Container, type SelectItem, SelectList } from "@sayknow-cli/tui";
|
|
3
|
-
import { getSelectListTheme } from "../../modes/theme/theme";
|
|
4
|
-
import { getThinkingLevelMetadata } from "../../thinking";
|
|
1
|
+
import { ThinkingLevel, type ThinkingLevel as ThinkingLevelValue } from "@sayknow-cli/agent-core";
|
|
2
|
+
import { Container, type SelectItem, SelectList, Text } from "@sayknow-cli/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): SKC_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;
|
|
@@ -24,6 +24,7 @@ import { HookSelectorComponent } from "../../modes/components/hook-selector";
|
|
|
24
24
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
25
25
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
26
26
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
27
|
+
import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
|
|
27
28
|
import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
|
|
28
29
|
|
|
29
30
|
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();
|
|
@@ -81,6 +81,7 @@ import { SessionSelectorComponent } from "../components/session-selector";
|
|
|
81
81
|
import { SettingsSelectorComponent } from "../components/settings-selector";
|
|
82
82
|
import type { StatusLineSettings } from "../components/status-line";
|
|
83
83
|
import { ThemeSelectorComponent } from "../components/theme-selector";
|
|
84
|
+
import { ThinkingSelectorComponent } from "../components/thinking-selector";
|
|
84
85
|
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
85
86
|
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
86
87
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
@@ -328,6 +329,48 @@ export class SelectorController {
|
|
|
328
329
|
});
|
|
329
330
|
}
|
|
330
331
|
|
|
332
|
+
showEffortSelector(): void {
|
|
333
|
+
const availableLevels = [
|
|
334
|
+
ThinkingLevel.Inherit,
|
|
335
|
+
ThinkingLevel.Off,
|
|
336
|
+
...this.ctx.session.getAvailableThinkingLevels(),
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
this.showSelector(done => {
|
|
340
|
+
const selector = new ThinkingSelectorComponent(
|
|
341
|
+
this.ctx.session.thinkingLevel,
|
|
342
|
+
availableLevels,
|
|
343
|
+
selection => {
|
|
344
|
+
done();
|
|
345
|
+
|
|
346
|
+
const { level, persistDefault } = selection;
|
|
347
|
+
const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
|
|
348
|
+
const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
|
|
349
|
+
this.ctx.session.setThinkingLevel(levelToApply, persistDefault);
|
|
350
|
+
const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
|
|
351
|
+
const requestedLabel =
|
|
352
|
+
level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
|
|
353
|
+
const clampedSuffix =
|
|
354
|
+
effectiveLevel === levelToApply ? "" : ` Requested ${levelToApply}; effective ${effectiveLevel}.`;
|
|
355
|
+
|
|
356
|
+
this.ctx.statusLine.invalidate();
|
|
357
|
+
this.ctx.updateEditorBorderColor();
|
|
358
|
+
this.ctx.updateEditorTopBorder();
|
|
359
|
+
if (persistDefault) void this.ctx.notifyConfigChanged?.();
|
|
360
|
+
this.ctx.ui.requestRender();
|
|
361
|
+
const scopeLabel = persistDefault ? "Default reasoning effort" : "Reasoning effort";
|
|
362
|
+
this.ctx.showStatus(
|
|
363
|
+
`${scopeLabel} set to ${requestedLabel}. Effective effort: ${effectiveLevel}.${clampedSuffix}`,
|
|
364
|
+
);
|
|
365
|
+
},
|
|
366
|
+
() => {
|
|
367
|
+
done();
|
|
368
|
+
this.ctx.ui.requestRender();
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
return { component: selector, focus: selector };
|
|
372
|
+
});
|
|
373
|
+
}
|
|
331
374
|
showSettingsSelector(): void {
|
|
332
375
|
getAvailableThemes().then(availableThemes => {
|
|
333
376
|
this.showSelector(done => {
|
|
@@ -331,6 +331,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
331
331
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
332
332
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
333
333
|
locallySubmittedUserSignatures: Set<string> = new Set();
|
|
334
|
+
optimisticInjectedSignatures: Map<string, number> = new Map();
|
|
334
335
|
#pendingSubmittedInput: SubmittedUserInput | undefined;
|
|
335
336
|
#pendingSubmissionDispose: (() => void) | undefined;
|
|
336
337
|
lastSigintTime = 0;
|
|
@@ -2561,6 +2562,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2561
2562
|
this.#selectorController.showModelSelector(options);
|
|
2562
2563
|
}
|
|
2563
2564
|
|
|
2565
|
+
showEffortSelector(): void {
|
|
2566
|
+
this.#selectorController.showEffortSelector();
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2564
2569
|
showProviderOnboarding(): void {
|
|
2565
2570
|
this.#selectorController.showProviderOnboarding();
|
|
2566
2571
|
}
|
|
@@ -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;
|