@charzhu/openjaw-agent 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -204,7 +204,7 @@ var init_config = __esm({
204
204
  "src/config.ts"() {
205
205
  "use strict";
206
206
  init_packageRoot();
207
- DEFAULT_COPILOT_OAUTH_CLIENT_ID = "Ov23li8tweQw6odWQebz";
207
+ DEFAULT_COPILOT_OAUTH_CLIENT_ID = "Iv1.b507a08c87ecfe98";
208
208
  DEFAULT_CONFIG = {
209
209
  llm: {
210
210
  provider: "anthropic",
@@ -1884,6 +1884,85 @@ var init_openai = __esm({
1884
1884
  }
1885
1885
  });
1886
1886
 
1887
+ // src/copilot-token.ts
1888
+ function normalizeCopilotEnterpriseDomain(value) {
1889
+ return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
1890
+ }
1891
+ function copilotApiBase(enterpriseUrl) {
1892
+ return enterpriseUrl ? `https://copilot-api.${normalizeCopilotEnterpriseDomain(enterpriseUrl)}` : "https://api.githubcopilot.com";
1893
+ }
1894
+ function copilotTokenEndpoint(enterpriseUrl) {
1895
+ if (!enterpriseUrl) return "https://api.github.com/copilot_internal/v2/token";
1896
+ const domain = normalizeCopilotEnterpriseDomain(enterpriseUrl);
1897
+ const apiDomain = domain.startsWith("api.") ? domain : `api.${domain}`;
1898
+ return `https://${apiDomain}/copilot_internal/v2/token`;
1899
+ }
1900
+ function decodeQueryComponent(value) {
1901
+ return decodeURIComponent(value.replace(/\+/g, "%20"));
1902
+ }
1903
+ function copilotApiBaseFromToken(token) {
1904
+ for (const part of token.split(";")) {
1905
+ const rawProxyEndpoint = part.startsWith("proxy-ep=") ? part.slice("proxy-ep=".length) : void 0;
1906
+ if (!rawProxyEndpoint) continue;
1907
+ const proxyEndpoint = decodeQueryComponent(rawProxyEndpoint).trim().replace(/\/+$/, "");
1908
+ if (!proxyEndpoint) return void 0;
1909
+ if (proxyEndpoint.startsWith("http://") || proxyEndpoint.startsWith("https://")) return proxyEndpoint;
1910
+ const host = proxyEndpoint.startsWith("proxy.") ? `api.${proxyEndpoint.slice("proxy.".length)}` : proxyEndpoint.startsWith("api.") ? proxyEndpoint : `api.${proxyEndpoint}`;
1911
+ return `https://${host}`;
1912
+ }
1913
+ return void 0;
1914
+ }
1915
+ async function exchangeGitHubTokenForCopilotToken(githubAccess, enterpriseUrl, signal) {
1916
+ const res = await fetch(copilotTokenEndpoint(enterpriseUrl), {
1917
+ headers: {
1918
+ "Accept": "application/json",
1919
+ "Authorization": `Bearer ${githubAccess}`,
1920
+ "User-Agent": COPILOT_USER_AGENT,
1921
+ "Editor-Version": COPILOT_EDITOR_VERSION,
1922
+ "Editor-Plugin-Version": COPILOT_EDITOR_PLUGIN_VERSION,
1923
+ "Copilot-Integration-Id": COPILOT_INTEGRATION_ID
1924
+ },
1925
+ signal
1926
+ });
1927
+ const text = await res.text();
1928
+ if (!res.ok) {
1929
+ let message = text;
1930
+ try {
1931
+ const parsed = JSON.parse(text);
1932
+ message = parsed.error_description || parsed.message || text;
1933
+ } catch {
1934
+ }
1935
+ throw new Error(`GitHub Copilot token request failed: ${res.status}${message ? ` ${message.slice(0, 160)}` : ""}`);
1936
+ }
1937
+ const body = JSON.parse(text);
1938
+ if (!body.token || typeof body.expires_at !== "number") {
1939
+ throw new Error("GitHub Copilot token response was missing token or expires_at.");
1940
+ }
1941
+ return {
1942
+ githubAccess,
1943
+ copilotAccess: body.token,
1944
+ copilotExpires: body.expires_at * 1e3,
1945
+ copilotApiBaseUrl: copilotApiBaseFromToken(body.token) ?? copilotApiBase(enterpriseUrl),
1946
+ enterpriseUrl
1947
+ };
1948
+ }
1949
+ var COPILOT_USER_AGENT, COPILOT_EDITOR_VERSION, COPILOT_EDITOR_PLUGIN_VERSION, COPILOT_INTEGRATION_ID;
1950
+ var init_copilot_token = __esm({
1951
+ "src/copilot-token.ts"() {
1952
+ "use strict";
1953
+ COPILOT_USER_AGENT = "GitHubCopilotChat/0.35.0";
1954
+ COPILOT_EDITOR_VERSION = "vscode/1.107.0";
1955
+ COPILOT_EDITOR_PLUGIN_VERSION = "copilot-chat/0.35.0";
1956
+ COPILOT_INTEGRATION_ID = "vscode-chat";
1957
+ __name(normalizeCopilotEnterpriseDomain, "normalizeCopilotEnterpriseDomain");
1958
+ __name(copilotApiBase, "copilotApiBase");
1959
+ __name(copilotTokenEndpoint, "copilotTokenEndpoint");
1960
+ __name(decodeQueryComponent, "decodeQueryComponent");
1961
+ __name(copilotApiBaseFromToken, "copilotApiBaseFromToken");
1962
+ __name(exchangeGitHubTokenForCopilotToken, "exchangeGitHubTokenForCopilotToken");
1963
+ }
1964
+ });
1965
+
1887
1966
  // src/provider-auth.ts
1888
1967
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
1889
1968
  import { join as join5 } from "node:path";
@@ -2024,12 +2103,6 @@ var init_types = __esm({
2024
2103
  });
2025
2104
 
2026
2105
  // src/providers/copilot.ts
2027
- function normalizeCopilotEnterpriseDomain(value) {
2028
- return value.replace(/^https?:\/\//, "").replace(/\/+$/, "");
2029
- }
2030
- function copilotApiBase(enterpriseUrl) {
2031
- return enterpriseUrl ? `https://copilot-api.${normalizeCopilotEnterpriseDomain(enterpriseUrl)}` : "https://api.githubcopilot.com";
2032
- }
2033
2106
  function shouldUseResponsesApi(modelID) {
2034
2107
  const normalized = modelID.toLowerCase();
2035
2108
  const match = /^gpt-(\d+)/.exec(normalized);
@@ -2079,6 +2152,12 @@ function toResponsesTool(tool) {
2079
2152
  parameters: normalizeObjectSchema2(tool.parameters)
2080
2153
  };
2081
2154
  }
2155
+ function endpointSupported(endpoints, target) {
2156
+ return endpoints?.some((endpoint) => endpoint.trim().toLowerCase() === target) ?? false;
2157
+ }
2158
+ function copilotSupportsResponsesWebSocket(modelInfo) {
2159
+ return endpointSupported(modelInfo?.supportedEndpoints, "ws:/responses");
2160
+ }
2082
2161
  function toAnthropicTool(tool) {
2083
2162
  assertToolName(tool.name);
2084
2163
  return {
@@ -2113,6 +2192,16 @@ function safeJsonParse(value) {
2113
2192
  return {};
2114
2193
  }
2115
2194
  }
2195
+ async function loadWebSocket() {
2196
+ const { createRequire } = await import("node:module");
2197
+ return createRequire(import.meta.url)("ws");
2198
+ }
2199
+ function responsesWebSocketUrl(baseUrl) {
2200
+ const url = new URL(`${baseUrl.replace(/\/+$/, "")}/responses`);
2201
+ if (url.protocol === "https:") url.protocol = "wss:";
2202
+ else if (url.protocol === "http:") url.protocol = "ws:";
2203
+ return url.toString();
2204
+ }
2116
2205
  function valueAtPath(value, path3) {
2117
2206
  let current = value;
2118
2207
  for (const key of path3) {
@@ -2121,6 +2210,24 @@ function valueAtPath(value, path3) {
2121
2210
  }
2122
2211
  return current;
2123
2212
  }
2213
+ function firstStringAtAny(value, paths2) {
2214
+ for (const path3 of paths2) {
2215
+ const candidate = valueAtPath(value, path3);
2216
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
2217
+ }
2218
+ return void 0;
2219
+ }
2220
+ function valueContainsAny(value, needles) {
2221
+ if (typeof value === "string") {
2222
+ const normalized = value.toLowerCase();
2223
+ return needles.some((needle) => normalized.includes(needle));
2224
+ }
2225
+ if (Array.isArray(value)) return value.some((item) => valueContainsAny(item, needles));
2226
+ if (value && typeof value === "object") {
2227
+ return Object.values(value).some((item) => valueContainsAny(item, needles));
2228
+ }
2229
+ return false;
2230
+ }
2124
2231
  function firstPositiveIntegerAtAny(value, paths2) {
2125
2232
  for (const path3 of paths2) {
2126
2233
  const candidate = valueAtPath(value, path3);
@@ -2151,25 +2258,68 @@ function readCopilotOutputTokens(capabilities) {
2151
2258
  function readCopilotReasoningEfforts(capabilities) {
2152
2259
  return normalizeReasoningEfforts(valueAtPath(capabilities, ["supports", "reasoning_effort"]));
2153
2260
  }
2261
+ function copilotModelItems(body) {
2262
+ if (Array.isArray(body.data) && body.data.length > 0) return body.data;
2263
+ if (Array.isArray(body.models)) return body.models;
2264
+ return [];
2265
+ }
2266
+ function isDisabledPolicyValue(value) {
2267
+ return ["disabled", "blocked", "denied", "unavailable", "off"].includes(value.toLowerCase());
2268
+ }
2269
+ function policyAllows(policy) {
2270
+ if (policy === void 0 || policy === null) return true;
2271
+ if (typeof policy === "boolean") return policy;
2272
+ if (typeof policy === "string") return !isDisabledPolicyValue(policy);
2273
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) return true;
2274
+ const record = policy;
2275
+ if (record.enabled === false) return false;
2276
+ return !["state", "status", "result"].some((key) => {
2277
+ const value = record[key];
2278
+ return typeof value === "string" && isDisabledPolicyValue(value);
2279
+ });
2280
+ }
2281
+ function isChatCapable(model) {
2282
+ if (model.id?.toLowerCase().includes("embedding")) return false;
2283
+ if (!model.capabilities) return true;
2284
+ if (valueContainsAny(model.capabilities, ["embedding", "embeddings"])) return false;
2285
+ const kind = firstStringAtAny(model.capabilities, [["type"], ["kind"], ["family"]]);
2286
+ return !kind || !["embedding", "embeddings", "completion", "completions"].includes(kind.toLowerCase());
2287
+ }
2154
2288
  function buildReasoning(effort, modelInfo) {
2155
2289
  const supported = modelInfo?.supportedReasoningEfforts?.map((option) => option.effort) ?? [];
2156
2290
  const selected = effort && supported.includes(effort) ? effort : void 0;
2157
2291
  const effective = selected ?? modelInfo?.defaultReasoningEffort;
2158
2292
  return effective ? { effort: effective } : void 0;
2159
2293
  }
2294
+ function collectResponsesOutputItem(item, accumulator) {
2295
+ if (item.type === "message" && !accumulator.sawTextDelta) {
2296
+ for (const block of item.content ?? []) {
2297
+ if ((block.type === "output_text" || block.type === "text") && block.text) {
2298
+ accumulator.text = (accumulator.text ?? "") + block.text;
2299
+ }
2300
+ }
2301
+ } else if (item.type === "function_call" && item.name) {
2302
+ accumulator.toolCalls.push({
2303
+ id: item.call_id || item.id || `call_${accumulator.toolCalls.length}`,
2304
+ name: item.name,
2305
+ input: safeJsonParse(item.arguments)
2306
+ });
2307
+ }
2308
+ }
2160
2309
  function parseCopilotModels(body) {
2161
- const items = Array.isArray(body.data) ? body.data : [];
2310
+ const items = copilotModelItems(body);
2162
2311
  const models = [];
2163
2312
  for (const item of items) {
2164
- if (!item.id || item.model_picker_enabled === false || item.policy?.state === "disabled") continue;
2165
- const visionMedia = item.capabilities?.limits?.vision?.supported_media_types ?? [];
2313
+ if (!item.id || item.model_picker_enabled === false || !policyAllows(item.policy) || !isChatCapable(item)) continue;
2314
+ const visionMedia = valueAtPath(item.capabilities, ["limits", "vision", "supported_media_types"]);
2166
2315
  const supportedReasoning = readCopilotReasoningEfforts(item.capabilities);
2167
2316
  models.push({
2168
2317
  id: item.id,
2169
2318
  name: item.name,
2170
2319
  supportedEndpoints: item.supported_endpoints ?? [],
2171
- supportsVision: item.capabilities?.supports?.vision === true || visionMedia.some((media) => media.startsWith("image/")),
2172
- supportsToolCalls: item.capabilities?.supports?.tool_calls !== false,
2320
+ supportsResponsesWebSocket: copilotSupportsResponsesWebSocket({ supportedEndpoints: item.supported_endpoints ?? [] }),
2321
+ supportsVision: valueAtPath(item.capabilities, ["supports", "vision"]) === true || Array.isArray(visionMedia) && visionMedia.some((media) => typeof media === "string" && media.startsWith("image/")),
2322
+ supportsToolCalls: valueAtPath(item.capabilities, ["supports", "tool_calls"]) !== false,
2173
2323
  contextWindow: readCopilotContextWindow(item.capabilities),
2174
2324
  outputTokens: readCopilotOutputTokens(item.capabilities),
2175
2325
  supportedReasoningEfforts: supportedReasoning.map((effort) => ({ effort, description: effort })),
@@ -2178,32 +2328,46 @@ function parseCopilotModels(body) {
2178
2328
  }
2179
2329
  return models;
2180
2330
  }
2181
- var COPILOT_PROVIDER, USER_AGENT, OPENAI_TOOL_NAME2, CopilotProvider;
2331
+ var COPILOT_PROVIDER, USER_AGENT, OPENAI_TOOL_NAME2, COPILOT_TOKEN_EXPIRY_SAFETY_MARGIN_MS, RESPONSES_WEBSOCKET_CONNECT_TIMEOUT_MS, RESPONSES_WEBSOCKET_IDLE_TIMEOUT_MS, CopilotProvider;
2182
2332
  var init_copilot = __esm({
2183
2333
  "src/providers/copilot.ts"() {
2184
2334
  "use strict";
2335
+ init_copilot_token();
2185
2336
  init_provider_auth();
2186
2337
  init_types();
2338
+ init_copilot_token();
2187
2339
  COPILOT_PROVIDER = "github-copilot";
2188
2340
  USER_AGENT = "openjaw-agent/0.1.0";
2189
2341
  OPENAI_TOOL_NAME2 = /^[A-Za-z0-9_-]{1,64}$/;
2190
- __name(normalizeCopilotEnterpriseDomain, "normalizeCopilotEnterpriseDomain");
2191
- __name(copilotApiBase, "copilotApiBase");
2342
+ COPILOT_TOKEN_EXPIRY_SAFETY_MARGIN_MS = 6e4;
2343
+ RESPONSES_WEBSOCKET_CONNECT_TIMEOUT_MS = 1e4;
2344
+ RESPONSES_WEBSOCKET_IDLE_TIMEOUT_MS = 12e4;
2192
2345
  __name(shouldUseResponsesApi, "shouldUseResponsesApi");
2193
2346
  __name(normalizeObjectSchema2, "normalizeObjectSchema");
2194
2347
  __name(assertToolName, "assertToolName");
2195
2348
  __name(toChatTool, "toChatTool");
2196
2349
  __name(toResponsesTool, "toResponsesTool");
2350
+ __name(endpointSupported, "endpointSupported");
2351
+ __name(copilotSupportsResponsesWebSocket, "copilotSupportsResponsesWebSocket");
2197
2352
  __name(toAnthropicTool, "toAnthropicTool");
2198
2353
  __name(hasImageContent, "hasImageContent");
2199
2354
  __name(openAIUserContent, "openAIUserContent");
2200
2355
  __name(safeJsonParse, "safeJsonParse");
2356
+ __name(loadWebSocket, "loadWebSocket");
2357
+ __name(responsesWebSocketUrl, "responsesWebSocketUrl");
2201
2358
  __name(valueAtPath, "valueAtPath");
2359
+ __name(firstStringAtAny, "firstStringAtAny");
2360
+ __name(valueContainsAny, "valueContainsAny");
2202
2361
  __name(firstPositiveIntegerAtAny, "firstPositiveIntegerAtAny");
2203
2362
  __name(readCopilotContextWindow, "readCopilotContextWindow");
2204
2363
  __name(readCopilotOutputTokens, "readCopilotOutputTokens");
2205
2364
  __name(readCopilotReasoningEfforts, "readCopilotReasoningEfforts");
2365
+ __name(copilotModelItems, "copilotModelItems");
2366
+ __name(isDisabledPolicyValue, "isDisabledPolicyValue");
2367
+ __name(policyAllows, "policyAllows");
2368
+ __name(isChatCapable, "isChatCapable");
2206
2369
  __name(buildReasoning, "buildReasoning");
2370
+ __name(collectResponsesOutputItem, "collectResponsesOutputItem");
2207
2371
  __name(parseCopilotModels, "parseCopilotModels");
2208
2372
  CopilotProvider = class {
2209
2373
  static {
@@ -2212,6 +2376,7 @@ var init_copilot = __esm({
2212
2376
  name = COPILOT_PROVIDER;
2213
2377
  config;
2214
2378
  modelCache = null;
2379
+ responsesWebSocketDisabled = false;
2215
2380
  constructor(config) {
2216
2381
  this.config = config;
2217
2382
  }
@@ -2228,39 +2393,77 @@ var init_copilot = __esm({
2228
2393
  }
2229
2394
  async chat(options) {
2230
2395
  const modelInfo = await this.resolveModelInfo(this.config.model);
2231
- if (modelInfo?.supportedEndpoints.includes("/v1/messages") || this.config.model.toLowerCase().startsWith("claude-")) {
2232
- return this.chatAnthropicMessages(options);
2396
+ if (hasImageContent(options.messages) && modelInfo?.supportsVision === false) {
2397
+ throw new Error(`model ${this.config.model} does not support image input; switch to a vision-capable model before submitting an image`);
2233
2398
  }
2234
2399
  if (this.shouldRouteToResponses(modelInfo)) {
2235
2400
  return this.chatResponses(options, modelInfo);
2236
2401
  }
2402
+ if (this.shouldRouteToAnthropicMessages(modelInfo)) {
2403
+ return this.chatAnthropicMessages(options);
2404
+ }
2237
2405
  return this.chatCompletions(options);
2238
2406
  }
2239
2407
  shouldRouteToResponses(modelInfo) {
2240
2408
  if (this.config.use_responses_api === false) return false;
2241
2409
  if (modelInfo?.supportedEndpoints.length) {
2242
- return modelInfo.supportedEndpoints.includes("/responses");
2410
+ return endpointSupported(modelInfo.supportedEndpoints, "/responses");
2243
2411
  }
2244
2412
  return shouldUseResponsesApi(this.config.model);
2245
2413
  }
2414
+ shouldRouteToAnthropicMessages(modelInfo) {
2415
+ if (modelInfo?.supportedEndpoints.length) return false;
2416
+ return this.config.model.toLowerCase().startsWith("claude-");
2417
+ }
2418
+ copilotBaseUrl(credential) {
2419
+ if (this.config.base_url) return this.config.base_url.replace(/\/+$/, "");
2420
+ if (credential?.type === "oauth" && credential.copilotApiBaseUrl) return credential.copilotApiBaseUrl.replace(/\/+$/, "");
2421
+ const enterpriseUrl = this.config.copilot_enterprise_url || (credential?.type === "oauth" ? credential.enterpriseUrl : void 0);
2422
+ return copilotApiBase(enterpriseUrl);
2423
+ }
2424
+ async resolveCopilotAuth(signal) {
2425
+ if (this.config.api_key && this.config.api_key !== "proxy-token") {
2426
+ return { token: this.config.api_key, baseUrl: this.copilotBaseUrl() };
2427
+ }
2428
+ const credential = getProviderCredential(COPILOT_PROVIDER);
2429
+ if (credential?.type !== "oauth") {
2430
+ throw new Error("GitHub Copilot is not connected. Run /connect github-copilot first.");
2431
+ }
2432
+ if (credential.copilotAccess && credential.copilotExpires && credential.copilotExpires > Date.now() + COPILOT_TOKEN_EXPIRY_SAFETY_MARGIN_MS) {
2433
+ return { token: credential.copilotAccess, baseUrl: this.copilotBaseUrl(credential) };
2434
+ }
2435
+ const githubAccess = credential.githubAccess || credential.refresh || credential.access;
2436
+ const enterpriseUrl = this.config.copilot_enterprise_url || credential.enterpriseUrl;
2437
+ const exchanged = await exchangeGitHubTokenForCopilotToken(githubAccess, enterpriseUrl, signal);
2438
+ saveProviderCredential({
2439
+ ...credential,
2440
+ access: exchanged.copilotAccess,
2441
+ refresh: exchanged.githubAccess,
2442
+ expires: exchanged.copilotExpires,
2443
+ githubAccess: exchanged.githubAccess,
2444
+ copilotAccess: exchanged.copilotAccess,
2445
+ copilotExpires: exchanged.copilotExpires,
2446
+ copilotApiBaseUrl: exchanged.copilotApiBaseUrl,
2447
+ enterpriseUrl
2448
+ });
2449
+ return { token: exchanged.copilotAccess, baseUrl: this.config.base_url?.replace(/\/+$/, "") ?? exchanged.copilotApiBaseUrl };
2450
+ }
2246
2451
  resolveGitHubToken() {
2247
2452
  if (this.config.api_key && this.config.api_key !== "proxy-token") return this.config.api_key;
2248
2453
  const credential = getProviderCredential(COPILOT_PROVIDER);
2249
- if (credential?.type === "oauth") return credential.access || credential.refresh;
2454
+ if (credential?.type === "oauth") return credential.githubAccess || credential.refresh || credential.access;
2250
2455
  throw new Error("GitHub Copilot is not connected. Run /connect github-copilot first.");
2251
2456
  }
2252
- resolveCopilotToken() {
2253
- return this.resolveGitHubToken();
2457
+ async resolveCopilotToken(signal) {
2458
+ return (await this.resolveCopilotAuth(signal)).token;
2254
2459
  }
2255
- baseUrl() {
2256
- if (this.config.base_url) return this.config.base_url.replace(/\/+$/, "");
2257
- const credential = getProviderCredential(COPILOT_PROVIDER);
2258
- const enterpriseUrl = this.config.copilot_enterprise_url || (credential?.type === "oauth" ? credential.enterpriseUrl : void 0);
2259
- return copilotApiBase(enterpriseUrl);
2460
+ async baseUrl(signal) {
2461
+ return (await this.resolveCopilotAuth(signal)).baseUrl;
2260
2462
  }
2261
- async headers(options, extra) {
2463
+ async headers(options, extra, auth) {
2464
+ const resolvedAuth = auth ?? await this.resolveCopilotAuth(options.signal);
2262
2465
  const headers = {
2263
- "Authorization": `Bearer ${await this.resolveCopilotToken()}`,
2466
+ "Authorization": `Bearer ${resolvedAuth.token}`,
2264
2467
  "Content-Type": "application/json",
2265
2468
  "User-Agent": USER_AGENT,
2266
2469
  "Editor-Version": "OpenJaw/0.1.0",
@@ -2274,15 +2477,17 @@ var init_copilot = __esm({
2274
2477
  return headers;
2275
2478
  }
2276
2479
  async fetchModelInfo() {
2277
- const res = await fetch(`${this.baseUrl()}/models`, {
2480
+ const signal = AbortSignal.timeout(1e4);
2481
+ const auth = await this.resolveCopilotAuth(signal);
2482
+ const res = await fetch(`${auth.baseUrl}/models`, {
2278
2483
  headers: {
2279
- "Authorization": `Bearer ${await this.resolveCopilotToken()}`,
2484
+ "Authorization": `Bearer ${auth.token}`,
2280
2485
  "User-Agent": USER_AGENT,
2281
2486
  "Editor-Version": "OpenJaw/0.1.0",
2282
2487
  "Editor-Plugin-Version": "OpenJaw/0.1.0",
2283
2488
  "Copilot-Integration-Id": "vscode-chat"
2284
2489
  },
2285
- signal: AbortSignal.timeout(1e4)
2490
+ signal
2286
2491
  });
2287
2492
  if (!res.ok) {
2288
2493
  const detail = await res.text().catch(() => "");
@@ -2304,6 +2509,7 @@ var init_copilot = __esm({
2304
2509
  id: modelID,
2305
2510
  name: modelID,
2306
2511
  supportedEndpoints: ["/v1/messages"],
2512
+ supportsResponsesWebSocket: false,
2307
2513
  supportsVision: true,
2308
2514
  supportsToolCalls: true
2309
2515
  };
@@ -2311,6 +2517,102 @@ var init_copilot = __esm({
2311
2517
  return null;
2312
2518
  }
2313
2519
  }
2520
+ shouldUseResponsesWebSocket(modelInfo) {
2521
+ return !this.responsesWebSocketDisabled && copilotSupportsResponsesWebSocket(modelInfo);
2522
+ }
2523
+ disableResponsesWebSocket() {
2524
+ this.responsesWebSocketDisabled = true;
2525
+ }
2526
+ buildResponsesRequestBody(options, modelInfo) {
2527
+ const reasoning = buildReasoning(options.reasoningEffort, modelInfo);
2528
+ return {
2529
+ model: this.config.model,
2530
+ input: this.buildResponsesInput(options),
2531
+ instructions: Array.isArray(options.systemPrompt) ? options.systemPrompt.map((block) => block.text).join("\n\n") : options.systemPrompt,
2532
+ tools: options.tools.length > 0 ? options.tools.map(toResponsesTool) : void 0,
2533
+ ...reasoning && { reasoning, include: ["reasoning.encrypted_content"] }
2534
+ };
2535
+ }
2536
+ buildResponsesWebSocketRequest(requestBody) {
2537
+ return {
2538
+ type: "response.create",
2539
+ ...requestBody,
2540
+ tools: requestBody.tools ?? [],
2541
+ tool_choice: requestBody.tools?.length ? "auto" : "none",
2542
+ parallel_tool_calls: true,
2543
+ store: false,
2544
+ stream: true,
2545
+ include: requestBody.include ?? []
2546
+ };
2547
+ }
2548
+ async chatResponsesWebSocket(options, requestBody) {
2549
+ const auth = await this.resolveCopilotAuth(options.signal);
2550
+ const headers = await this.headers(options, void 0, auth);
2551
+ const WebSocket2 = await loadWebSocket();
2552
+ const ws = new WebSocket2(responsesWebSocketUrl(auth.baseUrl), {
2553
+ headers,
2554
+ handshakeTimeout: RESPONSES_WEBSOCKET_CONNECT_TIMEOUT_MS
2555
+ });
2556
+ const request = JSON.stringify(this.buildResponsesWebSocketRequest(requestBody));
2557
+ return new Promise((resolve5, reject) => {
2558
+ const accumulator = { sawTextDelta: false, text: null, toolCalls: [] };
2559
+ let settled = false;
2560
+ const timeout = setTimeout(() => {
2561
+ if (settled) return;
2562
+ settled = true;
2563
+ ws.close();
2564
+ reject(new Error("Responses WebSocket idle timeout"));
2565
+ }, RESPONSES_WEBSOCKET_IDLE_TIMEOUT_MS);
2566
+ const finish = /* @__PURE__ */ __name((value) => {
2567
+ if (settled) return;
2568
+ settled = true;
2569
+ clearTimeout(timeout);
2570
+ ws.close();
2571
+ resolve5(value);
2572
+ }, "finish");
2573
+ const fail = /* @__PURE__ */ __name((error) => {
2574
+ if (settled) return;
2575
+ settled = true;
2576
+ clearTimeout(timeout);
2577
+ ws.close();
2578
+ reject(error instanceof Error ? error : new Error(String(error)));
2579
+ }, "fail");
2580
+ options.signal?.addEventListener("abort", () => fail(options.signal?.reason ?? new DOMException("Aborted", "AbortError")), { once: true });
2581
+ ws.addEventListener("open", () => ws.send(request));
2582
+ ws.addEventListener("error", (event) => fail(event.error ?? event.message ?? "Responses WebSocket error"));
2583
+ ws.addEventListener("close", (event) => {
2584
+ if (!settled) fail(new Error(`Responses WebSocket closed before completion (${event.code ?? "unknown"})`));
2585
+ });
2586
+ ws.addEventListener("message", (event) => {
2587
+ try {
2588
+ const text = typeof event.data === "string" ? event.data : Buffer.isBuffer(event.data) ? event.data.toString("utf8") : String(event.data);
2589
+ const parsed = JSON.parse(text);
2590
+ const type = String(parsed.type ?? "");
2591
+ if (type === "response.output_text.delta" && typeof parsed.delta === "string") {
2592
+ accumulator.sawTextDelta = true;
2593
+ accumulator.text = (accumulator.text ?? "") + parsed.delta;
2594
+ } else if (type === "response.output_item.done" && parsed.item && typeof parsed.item === "object") {
2595
+ collectResponsesOutputItem(parsed.item, accumulator);
2596
+ } else if (type === "response.failed") {
2597
+ fail(new Error(JSON.stringify(parsed.response ?? parsed)));
2598
+ } else if (type === "response.incomplete") {
2599
+ fail(new Error("Responses WebSocket returned incomplete response"));
2600
+ } else if (type === "response.completed") {
2601
+ const response = parsed.response && typeof parsed.response === "object" ? parsed.response : {};
2602
+ const usage2 = response.usage && typeof response.usage === "object" ? response.usage : void 0;
2603
+ finish({
2604
+ text: accumulator.text,
2605
+ toolCalls: accumulator.toolCalls,
2606
+ stopReason: accumulator.toolCalls.length > 0 ? "tool_use" : "end",
2607
+ usage: this.usage(usage2?.input_tokens, usage2?.output_tokens)
2608
+ });
2609
+ }
2610
+ } catch (error) {
2611
+ fail(error);
2612
+ }
2613
+ });
2614
+ });
2615
+ }
2314
2616
  buildChatMessages(options) {
2315
2617
  const messages = [
2316
2618
  {
@@ -2362,7 +2664,7 @@ var init_copilot = __esm({
2362
2664
  tool_choice: options.tools.length > 0 ? "auto" : void 0,
2363
2665
  temperature: this.config.temperature
2364
2666
  };
2365
- const res = await fetch(`${this.baseUrl()}/chat/completions`, {
2667
+ const res = await fetch(`${await this.baseUrl(options.signal)}/chat/completions`, {
2366
2668
  method: "POST",
2367
2669
  headers: await this.headers(options),
2368
2670
  body: JSON.stringify(requestBody),
@@ -2406,15 +2708,16 @@ var init_copilot = __esm({
2406
2708
  return input;
2407
2709
  }
2408
2710
  async chatResponses(options, modelInfo) {
2409
- const reasoning = buildReasoning(options.reasoningEffort, modelInfo);
2410
- const requestBody = {
2411
- model: this.config.model,
2412
- input: this.buildResponsesInput(options),
2413
- instructions: Array.isArray(options.systemPrompt) ? options.systemPrompt.map((block) => block.text).join("\n\n") : options.systemPrompt,
2414
- tools: options.tools.length > 0 ? options.tools.map(toResponsesTool) : void 0,
2415
- ...reasoning && { reasoning, include: ["reasoning.encrypted_content"] }
2416
- };
2417
- const res = await fetch(`${this.baseUrl()}/responses`, {
2711
+ const requestBody = this.buildResponsesRequestBody(options, modelInfo);
2712
+ if (this.shouldUseResponsesWebSocket(modelInfo)) {
2713
+ try {
2714
+ return await this.chatResponsesWebSocket(options, requestBody);
2715
+ } catch (err) {
2716
+ if (options.signal?.aborted) throw err;
2717
+ this.disableResponsesWebSocket();
2718
+ }
2719
+ }
2720
+ const res = await fetch(`${await this.baseUrl(options.signal)}/responses`, {
2418
2721
  method: "POST",
2419
2722
  headers: await this.headers(options),
2420
2723
  body: JSON.stringify(requestBody),
@@ -2493,7 +2796,7 @@ var init_copilot = __esm({
2493
2796
  messages: this.buildAnthropicMessages(options),
2494
2797
  tools: options.tools.length > 0 ? options.tools.map(toAnthropicTool) : void 0
2495
2798
  };
2496
- const res = await fetch(`${this.baseUrl()}/v1/messages`, {
2799
+ const res = await fetch(`${await this.baseUrl(options.signal)}/v1/messages`, {
2497
2800
  method: "POST",
2498
2801
  headers: await this.headers(options, {
2499
2802
  "anthropic-version": "2023-06-01",
@@ -3825,13 +4128,18 @@ async function completeCopilotDeviceFlow(flow, signal) {
3825
4128
  }
3826
4129
  const body = await res.json();
3827
4130
  if (body.access_token) {
4131
+ const copilotToken = await exchangeGitHubTokenForCopilotToken(body.access_token, flow.enterpriseUrl, signal);
3828
4132
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
3829
4133
  saveProviderCredential({
3830
4134
  type: "oauth",
3831
4135
  provider: "github-copilot",
3832
- access: body.access_token,
3833
- refresh: body.access_token,
3834
- expires: 0,
4136
+ access: copilotToken.copilotAccess,
4137
+ refresh: copilotToken.githubAccess,
4138
+ expires: copilotToken.copilotExpires,
4139
+ githubAccess: copilotToken.githubAccess,
4140
+ copilotAccess: copilotToken.copilotAccess,
4141
+ copilotExpires: copilotToken.copilotExpires,
4142
+ copilotApiBaseUrl: copilotToken.copilotApiBaseUrl,
3835
4143
  enterpriseUrl: flow.enterpriseUrl,
3836
4144
  createdAt: now2,
3837
4145
  updatedAt: now2
@@ -3839,7 +4147,7 @@ async function completeCopilotDeviceFlow(flow, signal) {
3839
4147
  return {
3840
4148
  provider: "github-copilot",
3841
4149
  enterpriseUrl: flow.enterpriseUrl,
3842
- baseUrl: copilotApiBase(flow.enterpriseUrl)
4150
+ baseUrl: copilotToken.copilotApiBaseUrl
3843
4151
  };
3844
4152
  }
3845
4153
  if (body.error === "authorization_pending") continue;
@@ -3855,7 +4163,7 @@ var init_copilot_auth = __esm({
3855
4163
  "src/copilot-auth.ts"() {
3856
4164
  "use strict";
3857
4165
  init_provider_auth();
3858
- init_copilot();
4166
+ init_copilot_token();
3859
4167
  OAUTH_POLLING_SAFETY_MARGIN_MS = 3e3;
3860
4168
  __name(isOAuthAbortError, "isOAuthAbortError");
3861
4169
  __name(oauthDomain, "oauthDomain");
@@ -4786,7 +5094,9 @@ ${summary}
4786
5094
  if (imageData) {
4787
5095
  const contentBlocks = [
4788
5096
  { type: "image", source: { type: "base64", media_type: imageData.mimeType, data: imageData.base64 } },
4789
- { type: "text", text: taskText }
5097
+ { type: "text", text: `${taskText}
5098
+
5099
+ [ATTACHED IMAGE]: The image is attached as visual input in this message. Inspect it directly; do not search the filesystem for an uploaded image path unless the user explicitly asks for a file.` }
4790
5100
  ];
4791
5101
  this.conversationHistory.push({ role: "user", content: contentBlocks });
4792
5102
  } else {
@@ -21921,11 +22231,31 @@ async function runCopilotResponsesSearch(input, llm, startedAt) {
21921
22231
  if (!credential || credential.type !== "oauth") {
21922
22232
  throw new Error("GitHub Copilot is not connected. Run /connect github-copilot first.");
21923
22233
  }
21924
- const baseUrl = llm.base_url?.replace(/\/+$/, "") ?? copilotApiBase(credential.enterpriseUrl);
22234
+ let copilotAccess = credential.copilotAccess;
22235
+ let copilotApiBaseUrl = credential.copilotApiBaseUrl;
22236
+ if (!copilotAccess || !credential.copilotExpires || credential.copilotExpires <= Date.now() + COPILOT_TOKEN_EXPIRY_SAFETY_MARGIN_MS2) {
22237
+ const exchanged = await exchangeGitHubTokenForCopilotToken(
22238
+ credential.githubAccess || credential.refresh || credential.access,
22239
+ credential.enterpriseUrl
22240
+ );
22241
+ copilotAccess = exchanged.copilotAccess;
22242
+ copilotApiBaseUrl = exchanged.copilotApiBaseUrl;
22243
+ saveProviderCredential({
22244
+ ...credential,
22245
+ access: exchanged.copilotAccess,
22246
+ refresh: exchanged.githubAccess,
22247
+ expires: exchanged.copilotExpires,
22248
+ githubAccess: exchanged.githubAccess,
22249
+ copilotAccess: exchanged.copilotAccess,
22250
+ copilotExpires: exchanged.copilotExpires,
22251
+ copilotApiBaseUrl: exchanged.copilotApiBaseUrl
22252
+ });
22253
+ }
22254
+ const baseUrl = llm.base_url?.replace(/\/+$/, "") ?? copilotApiBaseUrl?.replace(/\/+$/, "") ?? copilotApiBase(credential.enterpriseUrl);
21925
22255
  const headers = {
21926
- "Authorization": `Bearer ${credential.access || credential.refresh}`,
22256
+ "Authorization": `Bearer ${copilotAccess}`,
21927
22257
  "Content-Type": "application/json",
21928
- "User-Agent": COPILOT_USER_AGENT,
22258
+ "User-Agent": COPILOT_USER_AGENT2,
21929
22259
  "Editor-Version": "OpenJaw/0.1.0",
21930
22260
  "Editor-Plugin-Version": "OpenJaw/0.1.0",
21931
22261
  "Copilot-Integration-Id": "vscode-chat",
@@ -22152,15 +22482,16 @@ function limitAndDedupe(results, max) {
22152
22482
  function truncate(text) {
22153
22483
  return text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
22154
22484
  }
22155
- var COPILOT_USER_AGENT, DEFAULT_TIMEOUT_MS;
22485
+ var COPILOT_USER_AGENT2, DEFAULT_TIMEOUT_MS, COPILOT_TOKEN_EXPIRY_SAFETY_MARGIN_MS2;
22156
22486
  var init_web_search = __esm({
22157
22487
  "src/web-search.ts"() {
22158
22488
  "use strict";
22159
22489
  init_web_search_types();
22490
+ init_copilot_token();
22160
22491
  init_provider_auth();
22161
- init_copilot();
22162
- COPILOT_USER_AGENT = "openjaw-agent/0.1.0";
22492
+ COPILOT_USER_AGENT2 = "openjaw-agent/0.1.0";
22163
22493
  DEFAULT_TIMEOUT_MS = 45e3;
22494
+ COPILOT_TOKEN_EXPIRY_SAFETY_MARGIN_MS2 = 6e4;
22164
22495
  __name(createWebSearchExecutor, "createWebSearchExecutor");
22165
22496
  __name(runCopilotResponsesSearch, "runCopilotResponsesSearch");
22166
22497
  __name(runOpenAIResponsesSearch, "runOpenAIResponsesSearch");
@@ -48335,14 +48666,26 @@ ${helpMessage}` : field.label;
48335
48666
  bus.log("info", `session.steer ${String(params.text ?? "").slice(0, 200)}`);
48336
48667
  return { status: "queued", text: String(params.text ?? "") };
48337
48668
  });
48669
+ let pendingImageSeq = 0;
48338
48670
  let pendingImage = null;
48671
+ const nextImageAttachmentId = /* @__PURE__ */ __name(() => `img-${Date.now().toString(36)}-${++pendingImageSeq}`, "nextImageAttachmentId");
48339
48672
  bus.registerRpc("prompt.submit", async (params) => {
48340
48673
  const text = String(params.text ?? "");
48341
48674
  if (!text) return { ok: false };
48342
48675
  const systemPromptArr = await systemPromptFn();
48343
48676
  const systemPrompt = systemPromptArr.join("\n\n");
48344
- const imageData = pendingImage ? { base64: pendingImage.base64, mimeType: pendingImage.mimeType } : void 0;
48345
- pendingImage = null;
48677
+ const requestedImageId = String(params.image_attachment_id ?? "");
48678
+ const imageForSubmit = pendingImage && (!requestedImageId || pendingImage.attachmentId === requestedImageId) ? pendingImage : null;
48679
+ if (imageForSubmit) {
48680
+ const modelInfo = agentLoop.getActiveModelMetadata() ?? await agentLoop.listModels().then(() => agentLoop.getActiveModelMetadata()).catch(() => void 0);
48681
+ if (modelInfo?.supportsVision === false) {
48682
+ throw new Error(`model ${agentLoop.model} does not support image input; switch to a vision-capable model before submitting an image`);
48683
+ }
48684
+ }
48685
+ const imageData = imageForSubmit ? { base64: imageForSubmit.base64, mimeType: imageForSubmit.mimeType } : void 0;
48686
+ if (imageForSubmit) {
48687
+ pendingImage = null;
48688
+ }
48346
48689
  currentRun = { abort: /* @__PURE__ */ __name(() => agentLoop.abort(), "abort") };
48347
48690
  void streamAgentRun({ agentLoop, bus, systemPrompt, text, imageData }).finally(() => {
48348
48691
  currentRun = null;
@@ -48418,6 +48761,32 @@ ${helpMessage}` : field.label;
48418
48761
  });
48419
48762
  });
48420
48763
  bus.registerRpc("clipboard.paste", async () => {
48764
+ try {
48765
+ if (clipboardHasImage()) {
48766
+ const img = readClipboardImage();
48767
+ if (img) {
48768
+ const attachmentId = nextImageAttachmentId();
48769
+ pendingImage = {
48770
+ attachmentId,
48771
+ base64: img.base64,
48772
+ mimeType: img.mimeType,
48773
+ name: "clipboard.png"
48774
+ };
48775
+ const byteLength = Math.ceil(img.base64.length * 3 / 4);
48776
+ const tokenEstimate = Math.max(1, Math.ceil(byteLength / 750));
48777
+ return {
48778
+ attached: true,
48779
+ attachment_id: attachmentId,
48780
+ count: 1,
48781
+ height: img.height,
48782
+ name: "clipboard.png",
48783
+ token_estimate: tokenEstimate,
48784
+ width: img.width
48785
+ };
48786
+ }
48787
+ }
48788
+ } catch {
48789
+ }
48421
48790
  try {
48422
48791
  const text = await readClipboardText();
48423
48792
  return { attached: false, message: text ?? "" };
@@ -49042,13 +49411,16 @@ ${helpMessage}` : field.label;
49042
49411
  }
49043
49412
  const ext = extname4(path3).toLowerCase().replace(/^\./, "");
49044
49413
  const mimeType = ext === "png" ? "image/png" : ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/png";
49414
+ const attachmentId = nextImageAttachmentId();
49045
49415
  pendingImage = {
49416
+ attachmentId,
49046
49417
  base64: buffer.toString("base64"),
49047
49418
  mimeType,
49048
49419
  name: basename3(path3)
49049
49420
  };
49050
49421
  const tokenEstimate = Math.max(1, Math.ceil(buffer.byteLength / 750));
49051
49422
  return {
49423
+ attachment_id: attachmentId,
49052
49424
  height: 0,
49053
49425
  name: basename3(path3),
49054
49426
  remainder,
@@ -49056,6 +49428,13 @@ ${helpMessage}` : field.label;
49056
49428
  width: 0
49057
49429
  };
49058
49430
  });
49431
+ bus.registerRpc("image.clear", (params) => {
49432
+ const attachmentId = String(params.attachment_id ?? "");
49433
+ if (!attachmentId || pendingImage?.attachmentId === attachmentId) {
49434
+ pendingImage = null;
49435
+ }
49436
+ return { ok: true };
49437
+ });
49059
49438
  bus.registerRpc("process.stop", () => {
49060
49439
  const wasRunning = agentLoop.isRunning;
49061
49440
  agentLoop.abort();
@@ -49255,6 +49634,7 @@ var init_rpcHandlers = __esm({
49255
49634
  init_uiStore();
49256
49635
  init_catalog();
49257
49636
  init_clipboard();
49637
+ init_clipboard_image();
49258
49638
  init_models_static();
49259
49639
  init_providers();
49260
49640
  init_types();
@@ -52516,6 +52896,7 @@ function useComposerState({
52516
52896
  }) {
52517
52897
  const [input, setInput] = useState12("");
52518
52898
  const [inputBuf, setInputBuf] = useState12([]);
52899
+ const [attachedImage, setAttachedImage] = useState12(null);
52519
52900
  const [pasteSnips, setPasteSnips] = useState12([]);
52520
52901
  const isBlocked = useStore($isBlocked);
52521
52902
  const { querier } = use_stdin_default();
@@ -52533,14 +52914,25 @@ function useComposerState({
52533
52914
  } = useQueue();
52534
52915
  const { historyRef, historyIdx, setHistoryIdx, historyDraftRef, pushHistory } = useInputHistory();
52535
52916
  const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, isBlocked, gw);
52536
- const clearIn = useCallback7(() => {
52917
+ const clearAttachedImage = useCallback7(() => {
52918
+ const attachmentId = attachedImage?.attachment_id;
52919
+ setAttachedImage(null);
52920
+ if (attachmentId) {
52921
+ void gw.request("image.clear", { attachment_id: attachmentId }).catch(() => {
52922
+ });
52923
+ }
52924
+ }, [attachedImage?.attachment_id, gw]);
52925
+ const clearIn = useCallback7((options = {}) => {
52537
52926
  setInput("");
52538
52927
  setInputBuf([]);
52539
52928
  setPasteSnips([]);
52929
+ if (!options.keepAttachedImage) {
52930
+ clearAttachedImage();
52931
+ }
52540
52932
  setQueueEdit(null);
52541
52933
  setHistoryIdx(null);
52542
52934
  historyDraftRef.current = "";
52543
- }, [historyDraftRef, setQueueEdit, setHistoryIdx]);
52935
+ }, [clearAttachedImage, historyDraftRef, setQueueEdit, setHistoryIdx]);
52544
52936
  const handleResolvedPaste = useCallback7(
52545
52937
  async ({
52546
52938
  bracketed,
@@ -52563,6 +52955,13 @@ function useComposerState({
52563
52955
  session_id: sid
52564
52956
  });
52565
52957
  if (attached?.name) {
52958
+ setAttachedImage({
52959
+ attachment_id: attached.attachment_id,
52960
+ height: attached.height,
52961
+ name: attached.name,
52962
+ token_estimate: attached.token_estimate,
52963
+ width: attached.width
52964
+ });
52566
52965
  onImageAttached?.(attached);
52567
52966
  const remainder = attached.remainder?.trim() ?? "";
52568
52967
  if (!remainder) {
@@ -52615,7 +53014,7 @@ function useComposerState({
52615
53014
  }) => {
52616
53015
  if (hotkey) {
52617
53016
  const preferOsc52 = isRemoteShellSession(process.env);
52618
- const readPreferredText = preferOsc52 ? readOsc52Clipboard(querier).then(async (osc52Text) => {
53017
+ const readPreferredText = /* @__PURE__ */ __name(() => preferOsc52 ? readOsc52Clipboard(querier).then(async (osc52Text) => {
52619
53018
  if (isUsableClipboardText(osc52Text)) {
52620
53019
  return osc52Text;
52621
53020
  }
@@ -52625,8 +53024,12 @@ function useComposerState({
52625
53024
  return clipText;
52626
53025
  }
52627
53026
  return readOsc52Clipboard(querier);
52628
- });
52629
- return readPreferredText.then(async (preferredText) => {
53027
+ }), "readPreferredText");
53028
+ return Promise.resolve(onClipboardPaste(true)).then(async (imageAttached) => {
53029
+ if (imageAttached) {
53030
+ return null;
53031
+ }
53032
+ const preferredText = await readPreferredText();
52630
53033
  if (isUsableClipboardText(preferredText)) {
52631
53034
  return handleResolvedPaste({ bracketed: false, cursor, text: preferredText, value });
52632
53035
  }
@@ -52664,6 +53067,7 @@ function useComposerState({
52664
53067
  }, [input, inputBuf, submitRef]);
52665
53068
  const actions = useMemo6(
52666
53069
  () => ({
53070
+ clearAttachedImage,
52667
53071
  clearIn,
52668
53072
  dequeue,
52669
53073
  enqueue,
@@ -52674,6 +53078,7 @@ function useComposerState({
52674
53078
  replaceQueue: replaceQ,
52675
53079
  setCompIdx,
52676
53080
  setHistoryIdx,
53081
+ setAttachedImage,
52677
53082
  setInput,
52678
53083
  setInputBuf,
52679
53084
  setPasteSnips,
@@ -52681,6 +53086,7 @@ function useComposerState({
52681
53086
  syncQueue
52682
53087
  }),
52683
53088
  [
53089
+ clearAttachedImage,
52684
53090
  clearIn,
52685
53091
  dequeue,
52686
53092
  enqueue,
@@ -52707,6 +53113,7 @@ function useComposerState({
52707
53113
  );
52708
53114
  const state = useMemo6(
52709
53115
  () => ({
53116
+ attachedImage,
52710
53117
  compIdx,
52711
53118
  compReplace,
52712
53119
  completions,
@@ -52717,7 +53124,7 @@ function useComposerState({
52717
53124
  queueEditIdx,
52718
53125
  queuedDisplay
52719
53126
  }),
52720
- [compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]
53127
+ [attachedImage, compIdx, compReplace, completions, historyIdx, input, inputBuf, pasteSnips, queueEditIdx, queuedDisplay]
52721
53128
  );
52722
53129
  return {
52723
53130
  actions,
@@ -53306,6 +53713,9 @@ function useInputHandlers(ctx) {
53306
53713
  if (key.escape && terminal.hasSelection) {
53307
53714
  return clearSelection2();
53308
53715
  }
53716
+ if (key.escape && cState.attachedImage) {
53717
+ return cActions.clearAttachedImage();
53718
+ }
53309
53719
  if (key.escape && live.focusedPane === "transcript") {
53310
53720
  patchUiState({ focusedPane: "composer" });
53311
53721
  return;
@@ -53354,7 +53764,7 @@ function useInputHandlers(ctx) {
53354
53764
  sys: actions.sys
53355
53765
  });
53356
53766
  }
53357
- if (cState.input || cState.inputBuf.length) {
53767
+ if (cState.input || cState.inputBuf.length || cState.attachedImage) {
53358
53768
  return cActions.clearIn();
53359
53769
  }
53360
53770
  return actions.die();
@@ -53544,6 +53954,7 @@ function useSessionLifecycle(opts) {
53544
53954
  setHistoryItems([]);
53545
53955
  setLastUserMsg("");
53546
53956
  setStickyPrompt("");
53957
+ composerActions.clearAttachedImage();
53547
53958
  composerActions.setPasteSnips([]);
53548
53959
  evictInkCaches("half");
53549
53960
  }, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording]);
@@ -53556,6 +53967,7 @@ function useSessionLifecycle(opts) {
53556
53967
  setHistoryItems(info ? [introMsg(info)] : []);
53557
53968
  setStickyPrompt("");
53558
53969
  setLastUserMsg("");
53970
+ composerActions.clearAttachedImage();
53559
53971
  composerActions.setPasteSnips([]);
53560
53972
  patchTurnState({ activity: [] });
53561
53973
  patchUiState({ info, usage: usageFrom(info) });
@@ -53786,6 +54198,7 @@ function useSubmission(opts) {
53786
54198
  const expand = expandSnips(composerState.pasteSnips);
53787
54199
  const startSubmit = /* @__PURE__ */ __name((displayText, submitText, showUserMessage2 = true) => {
53788
54200
  const sid2 = getUiState().sid;
54201
+ const imageAttachment = composerState.attachedImage;
53789
54202
  if (!sid2) {
53790
54203
  return sys("session not ready yet");
53791
54204
  }
@@ -53798,7 +54211,20 @@ function useSubmission(opts) {
53798
54211
  patchUiState({ busy: true, status: "running\u2026" });
53799
54212
  turnController.bufRef = "";
53800
54213
  turnController.interrupted = false;
53801
- gw.request("prompt.submit", { session_id: sid2, text: submitText }).catch((e) => {
54214
+ const submitParams = { session_id: sid2, text: submitText };
54215
+ if (imageAttachment?.attachment_id) {
54216
+ submitParams.image_attachment_id = imageAttachment.attachment_id;
54217
+ }
54218
+ if (imageAttachment) {
54219
+ composerActions.setAttachedImage(null);
54220
+ }
54221
+ gw.request("prompt.submit", submitParams).catch((e) => {
54222
+ if (imageAttachment) {
54223
+ composerActions.setAttachedImage(imageAttachment);
54224
+ sys(`error: ${e.message}`);
54225
+ patchUiState({ busy: false, status: "ready" });
54226
+ return;
54227
+ }
53802
54228
  if (isSessionBusyError(e)) {
53803
54229
  composerActions.enqueue(submitText);
53804
54230
  patchUiState({ busy: true, status: "queued for next turn" });
@@ -53824,7 +54250,7 @@ function useSubmission(opts) {
53824
54250
  startSubmit(r.text || text, expand(r.text || text), showUserMessage);
53825
54251
  }).catch(() => startSubmit(text, expand(text), showUserMessage));
53826
54252
  },
53827
- [appendMessage, composerActions, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]
54253
+ [appendMessage, composerActions, composerState.attachedImage, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]
53828
54254
  );
53829
54255
  const shellExec = useCallback9(
53830
54256
  (cmd) => {
@@ -53887,6 +54313,16 @@ function useSubmission(opts) {
53887
54313
  }
53888
54314
  sys(note);
53889
54315
  }, "fallback");
54316
+ if (composerState.attachedImage) {
54317
+ if (live.sid) {
54318
+ turnController.interruptTurn({ appendMessage, gw, sid: live.sid, sys });
54319
+ }
54320
+ if (hasInterpolation(full)) {
54321
+ patchUiState({ busy: true });
54322
+ return interpolate(full, send);
54323
+ }
54324
+ return send(full);
54325
+ }
53890
54326
  if (mode === "queue") {
53891
54327
  return composerActions.enqueue(full);
53892
54328
  }
@@ -53908,7 +54344,7 @@ function useSubmission(opts) {
53908
54344
  }
53909
54345
  send(full);
53910
54346
  },
53911
- [appendMessage, composerActions, composerRefs, gw, interpolate, send, sys]
54347
+ [appendMessage, composerActions, composerRefs, composerState.attachedImage, gw, interpolate, send, sys]
53912
54348
  );
53913
54349
  const dispatchSubmission = useCallback9(
53914
54350
  (full) => {
@@ -53933,8 +54369,8 @@ function useSubmission(opts) {
53933
54369
  return;
53934
54370
  }
53935
54371
  const editIdx = composerRefs.queueEditRef.current;
53936
- composerActions.clearIn();
53937
54372
  if (editIdx !== null) {
54373
+ composerActions.clearIn();
53938
54374
  composerActions.replaceQueue(editIdx, full);
53939
54375
  const picked = composerRefs.queueRef.current.splice(editIdx, 1)[0];
53940
54376
  composerActions.syncQueue();
@@ -53951,6 +54387,7 @@ function useSubmission(opts) {
53951
54387
  }
53952
54388
  return sendQueued(picked);
53953
54389
  }
54390
+ composerActions.clearIn({ keepAttachedImage: !!composerState.attachedImage });
53954
54391
  composerActions.pushHistory(full);
53955
54392
  if (getUiState().busy) {
53956
54393
  return handleBusyInput(full);
@@ -53961,7 +54398,7 @@ function useSubmission(opts) {
53961
54398
  }
53962
54399
  send(full);
53963
54400
  },
53964
- [appendMessage, composerActions, composerRefs, handleBusyInput, interpolate, send, sendQueued, shellExec, slashRef]
54401
+ [appendMessage, composerActions, composerRefs, composerState.attachedImage, handleBusyInput, interpolate, send, sendQueued, shellExec, slashRef]
53965
54402
  );
53966
54403
  const submit = useCallback9(
53967
54404
  (value) => {
@@ -54079,8 +54516,7 @@ function useMainApp(gw) {
54079
54516
  const scrollRef = useRef13(null);
54080
54517
  const onEventRef = useRef13(() => {
54081
54518
  });
54082
- const clipboardPasteRef = useRef13(() => {
54083
- });
54519
+ const clipboardPasteRef = useRef13(() => false);
54084
54520
  const submitRef = useRef13(() => {
54085
54521
  });
54086
54522
  const terminalHintsShownRef = useRef13(/* @__PURE__ */ new Set());
@@ -54133,9 +54569,7 @@ function useMainApp(gw) {
54133
54569
  const composer = useComposerState({
54134
54570
  gw,
54135
54571
  onClipboardPaste: /* @__PURE__ */ __name((quiet) => clipboardPasteRef.current(quiet), "onClipboardPaste"),
54136
- onImageAttached: /* @__PURE__ */ __name((info) => {
54137
- sys(attachedImageNotice(info));
54138
- }, "onImageAttached"),
54572
+ onImageAttached: /* @__PURE__ */ __name(() => patchUiState({ status: "image attached" }), "onImageAttached"),
54139
54573
  submitRef
54140
54574
  });
54141
54575
  const { actions: composerActions, refs: composerRefs, state: composerState } = composer;
@@ -54343,17 +54777,25 @@ function useMainApp(gw) {
54343
54777
  const paste2 = useCallback10(
54344
54778
  (quiet = false) => rpc("clipboard.paste", { session_id: getUiState().sid }).then((r) => {
54345
54779
  if (!r) {
54346
- return;
54780
+ return false;
54347
54781
  }
54348
54782
  if (r.attached) {
54349
- const meta = imageTokenMeta(r);
54350
- return sys(`\u{1F4CE} Image #${r.count} attached from clipboard${meta ? ` \xB7 ${meta}` : ""}`);
54783
+ composerActions.setAttachedImage({
54784
+ attachment_id: r.attachment_id,
54785
+ height: r.height,
54786
+ name: r.name ?? `Image #${r.count ?? 1}`,
54787
+ token_estimate: r.token_estimate,
54788
+ width: r.width
54789
+ });
54790
+ patchUiState({ status: "image attached" });
54791
+ return true;
54351
54792
  }
54352
54793
  if (!quiet) {
54353
54794
  sys(r.message || "No image found in clipboard");
54354
54795
  }
54796
+ return false;
54355
54797
  }),
54356
- [rpc, sys]
54798
+ [composerActions, rpc, sys]
54357
54799
  );
54358
54800
  clipboardPasteRef.current = paste2;
54359
54801
  const { dispatchSubmission, send, sendQueued, submit } = useSubmission({
@@ -54593,6 +55035,7 @@ function useMainApp(gw) {
54593
55035
  cols,
54594
55036
  compIdx: composerState.compIdx,
54595
55037
  completions: composerState.completions,
55038
+ attachedImage: composerState.attachedImage,
54596
55039
  empty,
54597
55040
  handleTextPaste: composerActions.handleTextPaste,
54598
55041
  input: composerState.input,
@@ -54649,7 +55092,6 @@ var init_useMainApp = __esm({
54649
55092
  init_env();
54650
55093
  init_limits();
54651
55094
  init_details();
54652
- init_messages();
54653
55095
  init_paths();
54654
55096
  init_useGitBranch();
54655
55097
  init_useVirtualHistory();
@@ -61954,6 +62396,7 @@ var init_appLayout = __esm({
61954
62396
  init_env();
61955
62397
  init_limits();
61956
62398
  init_placeholders();
62399
+ init_messages();
61957
62400
  init_inputMetrics();
61958
62401
  init_perfPane();
61959
62402
  init_agentsOverlay();
@@ -62073,6 +62516,7 @@ var init_appLayout = __esm({
62073
62516
  const inputColumns = stableComposerColumns(composer.cols, promptWidth);
62074
62517
  const inputHeight = inputVisualHeight(composer.input, inputColumns);
62075
62518
  const inputMouseRef = useRef19(null);
62519
+ const attachedImageMeta = composer.attachedImage ? imageTokenMeta(composer.attachedImage) : "";
62076
62520
  const captureInputDrag = /* @__PURE__ */ __name((e) => {
62077
62521
  if (e.button !== 0) {
62078
62522
  return;
@@ -62143,6 +62587,12 @@ var init_appLayout = __esm({
62143
62587
  ),
62144
62588
  composer.input === "?" && !composer.inputBuf.length && /* @__PURE__ */ jsx41(HelpHint, { t: ui.theme }),
62145
62589
  !isBlocked && /* @__PURE__ */ jsxs28(Fragment11, { children: [
62590
+ composer.attachedImage && /* @__PURE__ */ jsx41(Box_default, { width: Math.max(1, composer.cols - 2), children: /* @__PURE__ */ jsxs28(Text9, { color: ui.theme.color.systemNote, wrap: "truncate-end", children: [
62591
+ "\u{1F4CE} ",
62592
+ composer.attachedImage.name,
62593
+ attachedImageMeta ? ` \xB7 ${attachedImageMeta}` : "",
62594
+ /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.muted, children: " \xB7 Esc to remove" })
62595
+ ] }) }),
62146
62596
  composer.inputBuf.map((line, i) => /* @__PURE__ */ jsxs28(Box_default, { children: [
62147
62597
  /* @__PURE__ */ jsx41(Box_default, { width: promptWidth, children: i === 0 ? /* @__PURE__ */ jsx41(PromptPrefix, { color: ui.theme.color.muted, promptText, width: promptWidth }) : /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.muted, children: promptBlank }) }),
62148
62598
  /* @__PURE__ */ jsx41(Text9, { color: ui.theme.color.composeText, children: line || " " })