@gajae-code/coding-agent 0.11.5 → 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 +67 -7
- 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 +4 -0
- 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 +13 -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 +35 -4
- package/dist/types/session/yield-queue.d.ts +8 -0
- 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 +3 -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 +71 -7
- 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 +9 -8
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +87 -28
- 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/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- 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/runtime-init.ts +16 -2
- 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 +20 -7
- package/src/sdk/bus/index.ts +8 -7
- 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 +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +26 -8
- package/src/sdk/bus/telegram-daemon.ts +1736 -338
- 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/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -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 +1212 -768
- 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 +385 -85
- package/src/session/yield-queue.ts +20 -0
- 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 +37 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask-answer-registry.ts +15 -1
- 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
|
}
|
|
@@ -1260,6 +1260,10 @@ export class CommandController {
|
|
|
1260
1260
|
}
|
|
1261
1261
|
|
|
1262
1262
|
async handleHandoffCommand(customInstructions?: string): Promise<void> {
|
|
1263
|
+
if (this.ctx.session.isStreaming) {
|
|
1264
|
+
this.ctx.showWarning("Wait for the current response to finish or abort it before handing off.");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1263
1267
|
const entries = this.ctx.sessionManager.getEntries();
|
|
1264
1268
|
const messageCount = entries.filter(e => e.type === "message").length;
|
|
1265
1269
|
|
|
@@ -1321,6 +1325,17 @@ export class CommandController {
|
|
|
1321
1325
|
} else {
|
|
1322
1326
|
this.ctx.showError(`Handoff failed: ${message}`);
|
|
1323
1327
|
}
|
|
1328
|
+
// The failure is non-destructive: the current session is unchanged. If the
|
|
1329
|
+
// document was already generated, preserve it on the clipboard for retry.
|
|
1330
|
+
const retainedDocument =
|
|
1331
|
+
error &&
|
|
1332
|
+
typeof error === "object" &&
|
|
1333
|
+
typeof (error as { handoffDocument?: unknown }).handoffDocument === "string"
|
|
1334
|
+
? (error as { handoffDocument: string }).handoffDocument
|
|
1335
|
+
: undefined;
|
|
1336
|
+
if (retainedDocument) {
|
|
1337
|
+
this.#doCopy(retainedDocument, "Handoff document preserved on clipboard; current session is unchanged.");
|
|
1338
|
+
}
|
|
1324
1339
|
} finally {
|
|
1325
1340
|
handoffLoader.stop();
|
|
1326
1341
|
this.ctx.statusContainer.clear();
|
|
@@ -214,13 +214,25 @@ export class ExtensionUiController {
|
|
|
214
214
|
try {
|
|
215
215
|
return {
|
|
216
216
|
handoff: await session.handoff(
|
|
217
|
-
typeof input.
|
|
217
|
+
typeof input.target === "string"
|
|
218
|
+
? input.target
|
|
219
|
+
: typeof input.instructions === "string"
|
|
220
|
+
? input.instructions
|
|
221
|
+
: undefined,
|
|
218
222
|
),
|
|
219
223
|
};
|
|
220
224
|
} catch (error) {
|
|
225
|
+
const typed = error as { code?: unknown; handoffDocument?: unknown };
|
|
226
|
+
const handoffDocument =
|
|
227
|
+
typeof typed?.handoffDocument === "string" ? { handoffDocument: typed.handoffDocument } : undefined;
|
|
228
|
+
// Preserve a safe typed code (e.g. transient `busy`) so clients keep
|
|
229
|
+
// correct retry/backoff semantics; only synthesize invalid_request for
|
|
230
|
+
// otherwise-untyped failures.
|
|
231
|
+
const code = typed?.code === "busy" ? "busy" : "invalid_request";
|
|
221
232
|
throw Object.assign(
|
|
222
233
|
new Error(error instanceof Error ? error.message : "Handoff is unavailable for the current state."),
|
|
223
|
-
{ code
|
|
234
|
+
{ code },
|
|
235
|
+
handoffDocument,
|
|
224
236
|
);
|
|
225
237
|
}
|
|
226
238
|
case "session.export_html":
|
|
@@ -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 {
|
|
@@ -301,15 +301,29 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
301
301
|
try {
|
|
302
302
|
return {
|
|
303
303
|
handoff: await session.handoff(
|
|
304
|
-
typeof input.
|
|
304
|
+
typeof input.target === "string"
|
|
305
|
+
? input.target
|
|
306
|
+
: typeof input.instructions === "string"
|
|
307
|
+
? input.instructions
|
|
308
|
+
: undefined,
|
|
305
309
|
),
|
|
306
310
|
};
|
|
307
311
|
} catch (error) {
|
|
312
|
+
const typed = error as { code?: unknown; handoffDocument?: unknown };
|
|
313
|
+
const handoffDocument =
|
|
314
|
+
typeof typed?.handoffDocument === "string"
|
|
315
|
+
? { handoffDocument: typed.handoffDocument }
|
|
316
|
+
: undefined;
|
|
317
|
+
// Preserve a safe typed code (e.g. transient `busy`) so clients keep
|
|
318
|
+
// correct retry/backoff semantics; only synthesize invalid_request for
|
|
319
|
+
// otherwise-untyped failures.
|
|
320
|
+
const code = typed?.code === "busy" ? "busy" : "invalid_request";
|
|
308
321
|
throw Object.assign(
|
|
309
322
|
new Error(
|
|
310
323
|
error instanceof Error ? error.message : "Handoff is unavailable for the current state.",
|
|
311
324
|
),
|
|
312
|
-
{ code
|
|
325
|
+
{ code },
|
|
326
|
+
handoffDocument,
|
|
313
327
|
);
|
|
314
328
|
}
|
|
315
329
|
case "session.export_html":
|
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
|
|