@gajae-code/coding-agent 0.11.6 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +56 -6
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +57 -6
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +86 -27
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/index.ts +4 -5
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +1663 -375
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +240 -149
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +319 -85
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import { prompt } from "@gajae-code/utils";
|
|
2
1
|
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
|
|
2
|
+
import type { BtwConversationScope, BtwTurnCapture } from "../../session/agent-session";
|
|
3
|
+
import {
|
|
4
|
+
BTW_MAX_QUESTION_UTF8_BYTES,
|
|
5
|
+
type BtwTextExchange,
|
|
6
|
+
boundBtwExchanges,
|
|
7
|
+
sanitizeBtwError,
|
|
8
|
+
utf8ByteLength,
|
|
9
|
+
} from "../../session/btw-contract";
|
|
3
10
|
import { BtwPanelComponent } from "../components/btw-panel";
|
|
4
11
|
import type { InteractiveModeContext } from "../types";
|
|
5
12
|
|
|
13
|
+
type BtwFollowUpResult = "accepted" | "busy" | "closed" | "rejected";
|
|
14
|
+
|
|
15
|
+
interface ActiveBtwTurn extends BtwTurnCapture {
|
|
16
|
+
abortController: AbortController | undefined;
|
|
17
|
+
active: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
interface BtwRequest {
|
|
7
21
|
component: BtwPanelComponent;
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
conversationScope: BtwConversationScope | undefined;
|
|
23
|
+
activeTurn: ActiveBtwTurn | undefined;
|
|
24
|
+
inFlight: boolean;
|
|
25
|
+
contextExchanges: BtwTextExchange[];
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
export class BtwController {
|
|
@@ -18,84 +34,151 @@ export class BtwController {
|
|
|
18
34
|
return this.#activeRequest !== undefined;
|
|
19
35
|
}
|
|
20
36
|
|
|
37
|
+
hasOpenPanel(): boolean {
|
|
38
|
+
return this.hasActiveRequest();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
isTurnInFlight(): boolean {
|
|
42
|
+
return this.#activeRequest?.inFlight ?? false;
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
handleEscape(): boolean {
|
|
22
46
|
if (!this.#activeRequest) return false;
|
|
23
|
-
this.#closeActiveRequest(
|
|
47
|
+
this.#closeActiveRequest();
|
|
24
48
|
return true;
|
|
25
49
|
}
|
|
26
50
|
|
|
27
51
|
dispose(): void {
|
|
28
|
-
this.#closeActiveRequest(
|
|
52
|
+
this.#closeActiveRequest();
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
async start(question: string): Promise<void> {
|
|
56
|
+
if (this.hasOpenPanel()) {
|
|
57
|
+
this.ctx.showStatus("A /btw chat is already open. Type a follow-up or press Esc to return to the main chat.");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
32
60
|
const trimmedQuestion = question.trim();
|
|
33
61
|
if (!trimmedQuestion) {
|
|
34
62
|
this.ctx.showStatus("Usage: /btw <question>");
|
|
35
63
|
return;
|
|
36
64
|
}
|
|
65
|
+
if (!this.#acceptQuestion(trimmedQuestion) || !this.#hasModel()) return;
|
|
66
|
+
const scope = this.ctx.session.createBtwConversationScope(btwUserPrompt);
|
|
67
|
+
const request = this.#openRequest(trimmedQuestion, scope);
|
|
68
|
+
void this.#runRequest(request);
|
|
69
|
+
}
|
|
37
70
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
71
|
+
async submitFollowUp(question: string): Promise<BtwFollowUpResult> {
|
|
72
|
+
const request = this.#activeRequest;
|
|
73
|
+
if (!request) return "closed";
|
|
74
|
+
if (request.inFlight) {
|
|
75
|
+
this.ctx.showStatus("The /btw chat is still answering. Wait for it to finish.");
|
|
76
|
+
return "busy";
|
|
42
77
|
}
|
|
78
|
+
const trimmedQuestion = question.trim();
|
|
79
|
+
if (!trimmedQuestion) return "closed";
|
|
80
|
+
if (!this.#acceptQuestion(trimmedQuestion)) return "rejected";
|
|
81
|
+
if (!request.conversationScope) return "closed";
|
|
82
|
+
request.activeTurn = this.#createTurn(trimmedQuestion, request.conversationScope);
|
|
83
|
+
request.inFlight = true;
|
|
84
|
+
request.component.beginTurn(trimmedQuestion);
|
|
85
|
+
void this.#runRequest(request);
|
|
86
|
+
return "accepted";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#acceptQuestion(question: string): boolean {
|
|
90
|
+
if (utf8ByteLength(question) <= BTW_MAX_QUESTION_UTF8_BYTES) return true;
|
|
91
|
+
this.ctx.showError(`/btw questions are limited to ${BTW_MAX_QUESTION_UTF8_BYTES} UTF-8 bytes.`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#hasModel(): boolean {
|
|
96
|
+
if (this.ctx.session.model) return true;
|
|
97
|
+
this.ctx.showError("No active model available for /btw.");
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
43
100
|
|
|
44
|
-
|
|
101
|
+
#createTurn(question: string, scope: BtwConversationScope): ActiveBtwTurn {
|
|
102
|
+
return { question, scope, abortController: new AbortController(), active: true };
|
|
103
|
+
}
|
|
45
104
|
|
|
105
|
+
#openRequest(question: string, scope: BtwConversationScope): BtwRequest {
|
|
46
106
|
const request: BtwRequest = {
|
|
47
|
-
component: new BtwPanelComponent({ question
|
|
48
|
-
|
|
49
|
-
|
|
107
|
+
component: new BtwPanelComponent({ question, tui: this.ctx.ui }),
|
|
108
|
+
conversationScope: scope,
|
|
109
|
+
activeTurn: this.#createTurn(question, scope),
|
|
110
|
+
inFlight: true,
|
|
111
|
+
contextExchanges: [],
|
|
50
112
|
};
|
|
51
113
|
this.ctx.btwContainer.clear();
|
|
52
114
|
this.ctx.btwContainer.addChild(request.component);
|
|
53
115
|
this.ctx.ui.requestRender();
|
|
54
116
|
this.#activeRequest = request;
|
|
55
|
-
|
|
117
|
+
return request;
|
|
56
118
|
}
|
|
57
119
|
|
|
58
120
|
async #runRequest(request: BtwRequest): Promise<void> {
|
|
121
|
+
const turn = request.activeTurn;
|
|
122
|
+
const abortController = turn?.abortController;
|
|
123
|
+
if (!turn?.scope || !abortController) return;
|
|
59
124
|
try {
|
|
60
|
-
const promptText = prompt.render(btwUserPrompt, { question: request.question });
|
|
61
125
|
const { replyText } = await this.ctx.session.runEphemeralTurn({
|
|
62
126
|
purpose: "btw",
|
|
63
|
-
|
|
127
|
+
turn,
|
|
128
|
+
contextExchanges: request.contextExchanges.map(exchange => ({ ...exchange })),
|
|
64
129
|
onTextDelta: delta => {
|
|
65
|
-
if (this.#isActiveRequest(request))
|
|
66
|
-
request.component.appendText(delta);
|
|
67
|
-
}
|
|
130
|
+
if (turn.active && this.#isActiveRequest(request)) request.component.appendText(delta);
|
|
68
131
|
},
|
|
69
|
-
signal:
|
|
132
|
+
signal: abortController.signal,
|
|
70
133
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
134
|
+
if (!turn.active || !this.#isActiveRequest(request)) return;
|
|
135
|
+
request.inFlight = false;
|
|
136
|
+
request.contextExchanges = boundBtwExchanges([
|
|
137
|
+
...request.contextExchanges,
|
|
138
|
+
{ question: turn.question, answer: replyText },
|
|
139
|
+
]);
|
|
140
|
+
if (replyText) request.component.setAnswer(replyText);
|
|
78
141
|
request.component.markComplete();
|
|
79
|
-
} catch
|
|
80
|
-
if (!this.#isActiveRequest(request))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (request.abortController.signal.aborted) {
|
|
142
|
+
} catch {
|
|
143
|
+
if (!turn.active || !this.#isActiveRequest(request)) return;
|
|
144
|
+
request.inFlight = false;
|
|
145
|
+
if (abortController.signal.aborted) {
|
|
84
146
|
request.component.markAborted();
|
|
85
147
|
return;
|
|
86
148
|
}
|
|
87
|
-
request.component.markError(
|
|
149
|
+
request.component.markError(sanitizeBtwError("Side-chat request failed."));
|
|
150
|
+
} finally {
|
|
151
|
+
if (request.activeTurn === turn) request.activeTurn = undefined;
|
|
152
|
+
this.#scrubTurn(turn);
|
|
88
153
|
}
|
|
89
154
|
}
|
|
90
155
|
|
|
91
|
-
#
|
|
156
|
+
#scrubTurn(turn: ActiveBtwTurn): void {
|
|
157
|
+
turn.active = false;
|
|
158
|
+
turn.question = "";
|
|
159
|
+
turn.abortController = undefined;
|
|
160
|
+
turn.scope = undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#closeActiveRequest(): void {
|
|
92
164
|
const request = this.#activeRequest;
|
|
93
165
|
if (!request) return;
|
|
94
166
|
this.#activeRequest = undefined;
|
|
95
|
-
|
|
96
|
-
|
|
167
|
+
const turn = request.activeTurn;
|
|
168
|
+
const abortController = turn?.abortController;
|
|
169
|
+
if (turn) this.#scrubTurn(turn);
|
|
170
|
+
request.activeTurn = undefined;
|
|
171
|
+
request.contextExchanges.splice(0);
|
|
172
|
+
request.inFlight = false;
|
|
173
|
+
if (request.conversationScope) {
|
|
174
|
+
request.conversationScope.messages.splice(0);
|
|
175
|
+
request.conversationScope.systemPrompt.splice(0);
|
|
176
|
+
request.conversationScope = undefined;
|
|
97
177
|
}
|
|
178
|
+
abortController?.abort();
|
|
98
179
|
request.component.close();
|
|
180
|
+
this.ctx.editor?.setText("");
|
|
181
|
+
this.ctx.pendingImages = [];
|
|
99
182
|
this.ctx.btwContainer.clear();
|
|
100
183
|
this.ctx.ui.requestRender();
|
|
101
184
|
}
|
|
@@ -752,6 +752,29 @@ export class InputController {
|
|
|
752
752
|
async submitText(text: string, composer: ComposerSubmissionOptions): Promise<void> {
|
|
753
753
|
text = text.trim();
|
|
754
754
|
if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
|
|
755
|
+
if (this.ctx.hasActiveBtw()) {
|
|
756
|
+
if (!text) return;
|
|
757
|
+
if (!text.startsWith("/")) {
|
|
758
|
+
const result = await this.ctx.handleBtwFollowUp(text);
|
|
759
|
+
if (result === "accepted" && this.#canModifyComposer(composer)) {
|
|
760
|
+
this.ctx.editor.setText("");
|
|
761
|
+
this.ctx.pendingImages = [];
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (/^\/btw(?:\s|$)/.test(text)) {
|
|
768
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
769
|
+
ctx: this.ctx,
|
|
770
|
+
handleBackgroundCommand: () => this.handleBackgroundCommand(),
|
|
771
|
+
composer,
|
|
772
|
+
});
|
|
773
|
+
if (slashResult === true) {
|
|
774
|
+
this.ctx.pendingImages = [];
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
755
778
|
|
|
756
779
|
// Empty submit while streaming with queued messages: flush queues immediately
|
|
757
780
|
if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
|
|
@@ -1357,6 +1380,14 @@ export class InputController {
|
|
|
1357
1380
|
async handleFollowUp(): Promise<void> {
|
|
1358
1381
|
const text = this.ctx.editor.getText().trim();
|
|
1359
1382
|
if (!text) return;
|
|
1383
|
+
// While /btw is open, plain text stays in the side chat. Slash-origin
|
|
1384
|
+
// input keeps normal dispatch so commands remain available.
|
|
1385
|
+
if (this.ctx.hasActiveBtw() && !text.startsWith("/")) {
|
|
1386
|
+
if ((await this.ctx.handleBtwFollowUp(text)) === "accepted") {
|
|
1387
|
+
this.ctx.editor.setText("");
|
|
1388
|
+
}
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1360
1391
|
|
|
1361
1392
|
// Compaction first: while compacting, queue free text and `/skill:*`
|
|
1362
1393
|
// commands in the compaction-local queue. `flushCompactionQueue`
|
|
@@ -101,8 +101,10 @@ import {
|
|
|
101
101
|
} from "../../setup/model-onboarding-guidance";
|
|
102
102
|
import { addApiCompatibleProvider, formatProviderSetupResult } from "../../setup/provider-onboarding";
|
|
103
103
|
import {
|
|
104
|
+
IMAGE_PROVIDER_DEFAULTS,
|
|
104
105
|
isConfigurableSearchProviderId,
|
|
105
106
|
isSearchProviderPreference,
|
|
107
|
+
setConfiguredImageModel,
|
|
106
108
|
setPreferredImageProvider,
|
|
107
109
|
setPreferredSearchProvider,
|
|
108
110
|
setSearchFallbackProviders,
|
|
@@ -190,6 +192,7 @@ export function buildStatusLineSettings(settingsInstance: Settings): StatusLineS
|
|
|
190
192
|
rightSegments: settingsInstance.get("statusLine.rightSegments"),
|
|
191
193
|
separator: settingsInstance.get("statusLine.separator"),
|
|
192
194
|
showHookStatus: settingsInstance.get("statusLine.showHookStatus"),
|
|
195
|
+
showActionHints: settingsInstance.get("statusLine.showActionHints"),
|
|
193
196
|
sessionAccent: settingsInstance.get("statusLine.sessionAccent"),
|
|
194
197
|
maxRows: settingsInstance.get("statusLine.maxRows"),
|
|
195
198
|
segmentOptions: settingsInstance.get("statusLine.segmentOptions"),
|
|
@@ -1103,6 +1106,68 @@ export class SelectorController {
|
|
|
1103
1106
|
}
|
|
1104
1107
|
}
|
|
1105
1108
|
|
|
1109
|
+
async #handleImageGenerationConfig(): Promise<void> {
|
|
1110
|
+
const provider = await this.ctx.showHookInput(
|
|
1111
|
+
"Image Generation provider (auto, openai, gemini, openrouter, antigravity, custom)",
|
|
1112
|
+
"auto",
|
|
1113
|
+
);
|
|
1114
|
+
if (provider === undefined) return;
|
|
1115
|
+
const normalized = provider.trim().toLowerCase();
|
|
1116
|
+
const validProviders = ["auto", "openai", "gemini", "openrouter", "antigravity", "custom"];
|
|
1117
|
+
if (!validProviders.includes(normalized)) {
|
|
1118
|
+
this.ctx.showStatus(`Invalid image provider: ${normalized}. Valid: ${validProviders.join(", ")}`);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
let model: string | undefined;
|
|
1122
|
+
if (normalized !== "auto" && normalized !== "custom") {
|
|
1123
|
+
const defaultModel = IMAGE_PROVIDER_DEFAULTS[normalized];
|
|
1124
|
+
model = await this.ctx.showHookInput(`Image model for ${normalized} (default: ${defaultModel})`, defaultModel);
|
|
1125
|
+
if (model === undefined) return;
|
|
1126
|
+
model = model.trim() || defaultModel;
|
|
1127
|
+
}
|
|
1128
|
+
let customUrl: string | undefined;
|
|
1129
|
+
let customKey: string | undefined;
|
|
1130
|
+
if (normalized === "custom") {
|
|
1131
|
+
customUrl = await this.ctx.showHookInput("Custom image endpoint base URL");
|
|
1132
|
+
if (!customUrl?.trim()) {
|
|
1133
|
+
this.ctx.showStatus("Custom image endpoint requires a base URL");
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
model = await this.ctx.showHookInput("Custom image model", IMAGE_PROVIDER_DEFAULTS.openai);
|
|
1137
|
+
if (model === undefined) return;
|
|
1138
|
+
model = model.trim() || IMAGE_PROVIDER_DEFAULTS.openai;
|
|
1139
|
+
customKey = await this.ctx.showHookInput("Custom image endpoint API key");
|
|
1140
|
+
}
|
|
1141
|
+
const scope = await this.ctx.showHookInput(
|
|
1142
|
+
"Scope: 'session' (this session only) or 'default' (persist)",
|
|
1143
|
+
"session",
|
|
1144
|
+
);
|
|
1145
|
+
if (scope === undefined) return;
|
|
1146
|
+
const persistDefault = scope.trim().toLowerCase() === "default";
|
|
1147
|
+
|
|
1148
|
+
const imageProvider = normalized as "auto" | "openai" | "gemini" | "openrouter" | "antigravity" | "custom";
|
|
1149
|
+
setPreferredImageProvider(imageProvider === "custom" ? "auto" : imageProvider);
|
|
1150
|
+
setConfiguredImageModel({
|
|
1151
|
+
provider: imageProvider,
|
|
1152
|
+
model: model ?? null,
|
|
1153
|
+
customUrl: customUrl?.trim(),
|
|
1154
|
+
customKey: customKey?.trim(),
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
if (persistDefault) {
|
|
1158
|
+
this.ctx.settings.set("providers.image", imageProvider);
|
|
1159
|
+
if (model) this.ctx.settings.set("providers.imageModel", model);
|
|
1160
|
+
if (customUrl?.trim()) this.ctx.settings.set("providers.imageCustomUrl", customUrl.trim());
|
|
1161
|
+
if (customKey?.trim()) this.ctx.settings.set("providers.imageCustomKey", customKey.trim());
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const displayModel =
|
|
1165
|
+
model ?? (normalized !== "auto" && normalized !== "custom" ? IMAGE_PROVIDER_DEFAULTS[normalized] : undefined);
|
|
1166
|
+
const label = normalized === "auto" ? "Auto" : `${normalized}${displayModel ? ` (${displayModel})` : ""}`;
|
|
1167
|
+
this.ctx.showStatus(`Image Generation: ${label}${persistDefault ? " (default)" : " (session)"}`);
|
|
1168
|
+
this.ctx.ui.requestRender();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1106
1171
|
showCustomProviderWizard(): void {
|
|
1107
1172
|
this.showSelector(done => {
|
|
1108
1173
|
let wizard: CustomProviderWizardComponent;
|
|
@@ -1516,6 +1581,7 @@ export class SelectorController {
|
|
|
1516
1581
|
case "statusLine.separator":
|
|
1517
1582
|
case "statusLineShowHooks":
|
|
1518
1583
|
case "statusLine.showHookStatus":
|
|
1584
|
+
case "statusLine.showActionHints":
|
|
1519
1585
|
case "statusLine.sessionAccent":
|
|
1520
1586
|
case "statusLine.maxRows":
|
|
1521
1587
|
case "statusLine.leftSegments":
|
|
@@ -1563,16 +1629,34 @@ export class SelectorController {
|
|
|
1563
1629
|
}
|
|
1564
1630
|
break;
|
|
1565
1631
|
case "providers.image":
|
|
1632
|
+
case "providers.imageModel":
|
|
1633
|
+
case "providers.imageCustomUrl":
|
|
1634
|
+
case "providers.imageCustomKey":
|
|
1635
|
+
case "providers.imageCustomKeyEnv": {
|
|
1636
|
+
const imgProvider = this.ctx.settings.get("providers.image");
|
|
1637
|
+
const imgModel = this.ctx.settings.get("providers.imageModel");
|
|
1638
|
+
const imgCustomUrl = this.ctx.settings.get("providers.imageCustomUrl");
|
|
1639
|
+
const imgCustomKey = this.ctx.settings.get("providers.imageCustomKey");
|
|
1640
|
+
const imgCustomKeyEnv = this.ctx.settings.get("providers.imageCustomKeyEnv");
|
|
1566
1641
|
if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1642
|
+
imgProvider === "auto" ||
|
|
1643
|
+
imgProvider === "openai" ||
|
|
1644
|
+
imgProvider === "gemini" ||
|
|
1645
|
+
imgProvider === "openrouter" ||
|
|
1646
|
+
imgProvider === "antigravity" ||
|
|
1647
|
+
imgProvider === "custom"
|
|
1572
1648
|
) {
|
|
1573
|
-
setPreferredImageProvider(
|
|
1649
|
+
setPreferredImageProvider(imgProvider === "custom" ? "auto" : imgProvider);
|
|
1650
|
+
setConfiguredImageModel({
|
|
1651
|
+
provider: imgProvider,
|
|
1652
|
+
model: imgModel ?? null,
|
|
1653
|
+
customUrl: imgCustomUrl,
|
|
1654
|
+
customKey: imgCustomKey,
|
|
1655
|
+
customKeyEnv: imgCustomKeyEnv,
|
|
1656
|
+
});
|
|
1574
1657
|
}
|
|
1575
1658
|
break;
|
|
1659
|
+
}
|
|
1576
1660
|
|
|
1577
1661
|
// MCP update injection - live subscribe/unsubscribe
|
|
1578
1662
|
case "mcp.notifications":
|
|
@@ -1631,6 +1715,11 @@ export class SelectorController {
|
|
|
1631
1715
|
await this.#deleteCustomModelPreset(selection.profileName, modelSelector);
|
|
1632
1716
|
return;
|
|
1633
1717
|
}
|
|
1718
|
+
if (selection.kind === "imageGeneration") {
|
|
1719
|
+
done();
|
|
1720
|
+
await this.#handleImageGenerationConfig();
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1634
1723
|
if (selection.kind === "profile") {
|
|
1635
1724
|
await this.#applyModelProfile(selection.profileName, selection.setDefault);
|
|
1636
1725
|
done();
|
|
@@ -2057,10 +2146,7 @@ export class SelectorController {
|
|
|
2057
2146
|
}
|
|
2058
2147
|
|
|
2059
2148
|
async showSessionSelector(): Promise<void> {
|
|
2060
|
-
const sessions = await
|
|
2061
|
-
this.ctx.sessionManager.getCwd(),
|
|
2062
|
-
this.ctx.sessionManager.getSessionDir(),
|
|
2063
|
-
);
|
|
2149
|
+
const sessions = await this.ctx.sessionManager.listForResumePickerReadOnly();
|
|
2064
2150
|
this.showSelector(done => {
|
|
2065
2151
|
const selector = new SessionSelectorComponent(
|
|
2066
2152
|
sessions,
|
|
@@ -2156,8 +2242,16 @@ export class SelectorController {
|
|
|
2156
2242
|
this.#clearTransientSessionUi();
|
|
2157
2243
|
const migrationPolicy =
|
|
2158
2244
|
this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
|
|
2159
|
-
|
|
2160
|
-
|
|
2245
|
+
let writableSessionPath = sessionPath;
|
|
2246
|
+
if (this.ctx.sessionManager.isManagedDestination()) {
|
|
2247
|
+
const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
|
|
2248
|
+
if (inspection.kind === "error") throw new Error(`Could not inspect selected session: ${inspection.reason}`);
|
|
2249
|
+
writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
|
|
2250
|
+
sessionPath,
|
|
2251
|
+
migrationPolicy,
|
|
2252
|
+
inspection.identity,
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2161
2255
|
// Switch session via AgentSession (emits hook and tool session events)
|
|
2162
2256
|
if (!(await this.ctx.session.switchSession(writableSessionPath))) return;
|
|
2163
2257
|
const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
@@ -264,6 +264,10 @@ export interface InteractiveModeOptions {
|
|
|
264
264
|
initialMessages?: string[];
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
export function selectShutdownDraft(editorText: string, hasActiveBtw: boolean): string {
|
|
268
|
+
return hasActiveBtw ? "" : editorText;
|
|
269
|
+
}
|
|
270
|
+
|
|
267
271
|
export class InteractiveMode implements InteractiveModeContext {
|
|
268
272
|
session: AgentSession;
|
|
269
273
|
sessionManager: SessionManager;
|
|
@@ -1322,19 +1326,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1322
1326
|
if (this.#isShuttingDown) return;
|
|
1323
1327
|
this.#isShuttingDown = true;
|
|
1324
1328
|
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
|
|
1328
|
-
const draftText = this.editor.getText();
|
|
1329
|
+
// `/btw` owns the shared composer while its panel is open. Never persist a
|
|
1330
|
+
// side-chat draft or pending side-chat images into the main-session draft.
|
|
1331
|
+
const hadActiveBtw = this.#btwController.hasOpenPanel();
|
|
1332
|
+
const draftText = selectShutdownDraft(this.editor.getText(), hadActiveBtw);
|
|
1333
|
+
if (hadActiveBtw) this.pendingImages = [];
|
|
1334
|
+
this.#btwController.dispose();
|
|
1329
1335
|
|
|
1330
|
-
// Flush pending session writes before shutdown
|
|
1336
|
+
// Flush pending session writes before shutdown.
|
|
1331
1337
|
await this.sessionManager.flush();
|
|
1332
1338
|
try {
|
|
1333
1339
|
await this.sessionManager.saveDraft(draftText);
|
|
1334
1340
|
} catch (err) {
|
|
1335
1341
|
logger.warn("Failed to save session draft", { error: String(err) });
|
|
1336
1342
|
}
|
|
1337
|
-
this.#btwController.dispose();
|
|
1338
1343
|
|
|
1339
1344
|
// Emit shutdown event to hooks
|
|
1340
1345
|
this.session.setSdkPlanModeHandler(null);
|
|
@@ -2025,7 +2030,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2025
2030
|
}
|
|
2026
2031
|
|
|
2027
2032
|
hasActiveBtw(): boolean {
|
|
2028
|
-
return this.#btwController.
|
|
2033
|
+
return this.#btwController.hasOpenPanel();
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
handleBtwFollowUp(question: string): Promise<"accepted" | "busy" | "closed" | "rejected"> {
|
|
2037
|
+
return this.#btwController.submitFollowUp(question);
|
|
2029
2038
|
}
|
|
2030
2039
|
|
|
2031
2040
|
handleBtwEscape(): boolean {
|
package/src/modes/types.ts
CHANGED
|
@@ -338,6 +338,7 @@ export interface InteractiveModeContext {
|
|
|
338
338
|
handleBackgroundCommand(): void;
|
|
339
339
|
handleImagePaste(): Promise<boolean>;
|
|
340
340
|
handleBtwCommand(question: string): Promise<void>;
|
|
341
|
+
handleBtwFollowUp(question: string): Promise<"accepted" | "busy" | "closed" | "rejected">;
|
|
341
342
|
hasActiveBtw(): boolean;
|
|
342
343
|
handleBtwEscape(): boolean;
|
|
343
344
|
cycleThinkingLevel(): void;
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Answer
|
|
4
|
-
Do not use tools.
|
|
5
|
-
Do not ask follow-up questions.
|
|
6
|
-
Question:
|
|
7
|
-
{{question}}
|
|
8
|
-
</btw>
|
|
1
|
+
You are answering inside an ephemeral multi-turn side chat for the interactive session.
|
|
2
|
+
Use only the sanitized visible-text conversation scope and prior side-chat turns provided as messages.
|
|
3
|
+
Answer directly. Do not use tools or request hidden session state.
|
|
@@ -3,6 +3,7 @@ Discover project and user runtime skills without loading full skill content.
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Searches only custom runtime skill locations: nearest project `.gjc/skills`; then, under the home directory, canonical `<config>/agent/skills`, configured legacy `<config>/skills`, and historical legacy `.gjc/skills`. `<config>` is the home-relative directory name from `GJC_CONFIG_DIR`, then `PI_CONFIG_DIR`, then `.gjc`; even an absolute-looking configured name is joined beneath `<home>`. Duplicate names use that exact precedence. Built-in, bundled, and internal workflow skills are intentionally excluded.
|
|
5
5
|
- Returns thin metadata only: name, description, source scope, path, and use conditions when present.
|
|
6
|
+
- When zero candidates are returned because discovery config is disabled (`skills.enabled`, `skills.enablePiProject`, `skills.enablePiUser`), the result carries a `notice` explaining which setting blocked the search — an empty result without a `notice` means the searched scopes genuinely contain no matching skills.
|
|
6
7
|
- To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.
|
|
7
8
|
</instruction>
|
|
8
9
|
|
|
@@ -15,15 +15,12 @@ import type {
|
|
|
15
15
|
MCPInitializeParams,
|
|
16
16
|
MCPInitializeResult,
|
|
17
17
|
MCPPrompt,
|
|
18
|
-
MCPPromptsListResult,
|
|
19
18
|
MCPRequestOptions,
|
|
20
19
|
MCPResource,
|
|
21
20
|
MCPResourceReadParams,
|
|
22
21
|
MCPResourceReadResult,
|
|
23
22
|
MCPResourceSubscribeParams,
|
|
24
|
-
MCPResourcesListResult,
|
|
25
23
|
MCPResourceTemplate,
|
|
26
|
-
MCPResourceTemplatesListResult,
|
|
27
24
|
MCPServerCapabilities,
|
|
28
25
|
MCPServerConfig,
|
|
29
26
|
MCPServerConnection,
|
|
@@ -43,6 +40,9 @@ const PROTOCOL_VERSION = "2025-03-26";
|
|
|
43
40
|
/** Default connection timeout in ms */
|
|
44
41
|
const CONNECTION_TIMEOUT_MS = 30_000;
|
|
45
42
|
|
|
43
|
+
const MAX_PAGINATION_PAGES = 100,
|
|
44
|
+
MAX_PAGINATION_ITEMS = 10_000;
|
|
45
|
+
|
|
46
46
|
/** Client info sent during initialization */
|
|
47
47
|
const CLIENT_INFO = {
|
|
48
48
|
name: "gjc-coding-agent",
|
|
@@ -109,6 +109,40 @@ function decodeToolsListResult(value: unknown): MCPToolsListResult {
|
|
|
109
109
|
return value.nextCursor === undefined ? { tools } : { tools, nextCursor: value.nextCursor };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
async function collectPaginated<T>(
|
|
113
|
+
connection: MCPServerConnection,
|
|
114
|
+
options: MCPRequestOptions | undefined,
|
|
115
|
+
method: string,
|
|
116
|
+
itemKey: string,
|
|
117
|
+
items: T[],
|
|
118
|
+
decode?: (value: unknown) => unknown,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const seenCursors = new Set<string>();
|
|
121
|
+
const failure = (detail: string) => new MCPExpectedFailure(new Error(`MCP ${method} pagination ${detail}`));
|
|
122
|
+
let cursor: string | undefined;
|
|
123
|
+
for (let page = 1; page <= MAX_PAGINATION_PAGES; page++) {
|
|
124
|
+
const value = await connection.transport.request<unknown>(method, cursor ? { cursor } : {}, options);
|
|
125
|
+
const result = decode ? decode(value) : value;
|
|
126
|
+
if (
|
|
127
|
+
!isRecord(result) ||
|
|
128
|
+
!Array.isArray(result[itemKey]) ||
|
|
129
|
+
(result.nextCursor !== undefined && typeof result.nextCursor !== "string")
|
|
130
|
+
)
|
|
131
|
+
throw new MCPExpectedFailure();
|
|
132
|
+
const nextCursor = result.nextCursor as string | undefined;
|
|
133
|
+
if (nextCursor && seenCursors.has(nextCursor)) throw failure("repeated a cursor");
|
|
134
|
+
const pageItems = result[itemKey] as T[];
|
|
135
|
+
const itemCount = items.length + pageItems.length;
|
|
136
|
+
if (itemCount > MAX_PAGINATION_ITEMS || (itemCount === MAX_PAGINATION_ITEMS && nextCursor))
|
|
137
|
+
throw failure("did not complete within the 10000-item budget");
|
|
138
|
+
items.push(...pageItems);
|
|
139
|
+
if (!nextCursor) return;
|
|
140
|
+
if (page === MAX_PAGINATION_PAGES) throw failure("did not complete within the 100-page budget");
|
|
141
|
+
seenCursors.add(nextCursor);
|
|
142
|
+
cursor = nextCursor;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
/**
|
|
113
147
|
* Default handler for standard MCP server-to-client requests.
|
|
114
148
|
* Handles `ping` and `roots/list`; rejects unknown methods with -32601.
|
|
@@ -292,18 +326,7 @@ export async function listTools(
|
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
const allTools: MCPToolDefinition[] = [];
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
do {
|
|
298
|
-
const params: Record<string, unknown> = {};
|
|
299
|
-
if (cursor) {
|
|
300
|
-
params.cursor = cursor;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const result = decodeToolsListResult(await connection.transport.request<unknown>("tools/list", params, options));
|
|
304
|
-
allTools.push(...result.tools);
|
|
305
|
-
cursor = result.nextCursor;
|
|
306
|
-
} while (cursor);
|
|
329
|
+
await collectPaginated(connection, options, "tools/list", "tools", allTools, decodeToolsListResult);
|
|
307
330
|
|
|
308
331
|
// Cache tools
|
|
309
332
|
connection.tools = allTools;
|
|
@@ -362,18 +385,7 @@ export async function listResources(
|
|
|
362
385
|
}
|
|
363
386
|
|
|
364
387
|
const allResources: MCPResource[] = [];
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
do {
|
|
368
|
-
const params: Record<string, unknown> = {};
|
|
369
|
-
if (cursor) {
|
|
370
|
-
params.cursor = cursor;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const result = await connection.transport.request<MCPResourcesListResult>("resources/list", params, options);
|
|
374
|
-
allResources.push(...result.resources);
|
|
375
|
-
cursor = result.nextCursor;
|
|
376
|
-
} while (cursor);
|
|
388
|
+
await collectPaginated(connection, options, "resources/list", "resources", allResources);
|
|
377
389
|
|
|
378
390
|
connection.resources = allResources;
|
|
379
391
|
return allResources;
|
|
@@ -395,22 +407,7 @@ export async function listResourceTemplates(
|
|
|
395
407
|
}
|
|
396
408
|
|
|
397
409
|
const allTemplates: MCPResourceTemplate[] = [];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
do {
|
|
401
|
-
const params: Record<string, unknown> = {};
|
|
402
|
-
if (cursor) {
|
|
403
|
-
params.cursor = cursor;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const result = await connection.transport.request<MCPResourceTemplatesListResult>(
|
|
407
|
-
"resources/templates/list",
|
|
408
|
-
params,
|
|
409
|
-
options,
|
|
410
|
-
);
|
|
411
|
-
allTemplates.push(...result.resourceTemplates);
|
|
412
|
-
cursor = result.nextCursor;
|
|
413
|
-
} while (cursor);
|
|
410
|
+
await collectPaginated(connection, options, "resources/templates/list", "resourceTemplates", allTemplates);
|
|
414
411
|
|
|
415
412
|
connection.resourceTemplates = allTemplates;
|
|
416
413
|
return allTemplates;
|
|
@@ -514,18 +511,7 @@ export async function listPrompts(
|
|
|
514
511
|
}
|
|
515
512
|
|
|
516
513
|
const allPrompts: MCPPrompt[] = [];
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
do {
|
|
520
|
-
const params: Record<string, unknown> = {};
|
|
521
|
-
if (cursor) {
|
|
522
|
-
params.cursor = cursor;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const result = await connection.transport.request<MCPPromptsListResult>("prompts/list", params, options);
|
|
526
|
-
allPrompts.push(...result.prompts);
|
|
527
|
-
cursor = result.nextCursor;
|
|
528
|
-
} while (cursor);
|
|
514
|
+
await collectPaginated(connection, options, "prompts/list", "prompts", allPrompts);
|
|
529
515
|
|
|
530
516
|
connection.prompts = allPrompts;
|
|
531
517
|
return allPrompts;
|