opencode-copilot-account-switcher 0.10.12 → 0.11.1

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.
@@ -75,7 +75,9 @@ export function rewriteModelAccountAssignments(store, rename) {
75
75
  const seen = new Set();
76
76
  const resolvedNames = [];
77
77
  for (const originalName of accountNames) {
78
- const mappedName = rename[originalName] ?? originalName;
78
+ const mappedName = Object.prototype.hasOwnProperty.call(rename, originalName)
79
+ ? rename[originalName]
80
+ : originalName;
79
81
  if (typeof mappedName !== "string" || !store.accounts[mappedName] || seen.has(mappedName))
80
82
  continue;
81
83
  seen.add(mappedName);
@@ -5,6 +5,7 @@ import { type StoreFile, type StoreWriteDebugMeta } from "./store.js";
5
5
  import { type CopilotAuthState, type CopilotProviderConfig, type OfficialCopilotConfig, type OfficialChatHeadersHook } from "./upstream/copilot-loader-adapter.js";
6
6
  import { type RefreshActiveAccountQuotaResult } from "./active-account-quota.js";
7
7
  import { handleStatusCommand } from "./status-command.js";
8
+ import { handleCompactCommand, handleStopToolCommand } from "./session-control-command.js";
8
9
  import { type AppendSessionTouchEventInput, type RouteDecisionEvent, type RoutingSnapshot, type RoutingEvent } from "./routing-state.js";
9
10
  type ChatHeadersHook = (input: {
10
11
  sessionID: string;
@@ -31,6 +32,8 @@ type CopilotPluginHooksWithChatHeaders = CopilotPluginHooks & {
31
32
  "chat.headers"?: ChatHeadersHook;
32
33
  };
33
34
  type StatusCommandHandler = typeof handleStatusCommand;
35
+ type CompactCommandHandler = typeof handleCompactCommand;
36
+ type StopToolCommandHandler = typeof handleStopToolCommand;
34
37
  type RefreshQuota = (store: StoreFile) => Promise<RefreshActiveAccountQuotaResult>;
35
38
  type CandidateAccountLoads = Record<string, number> | Map<string, number>;
36
39
  type TriggerBillingCompensationInput = {
@@ -79,6 +82,8 @@ export declare function buildPluginHooks(input: {
79
82
  now?: () => number;
80
83
  refreshQuota?: RefreshQuota;
81
84
  handleStatusCommandImpl?: StatusCommandHandler;
85
+ handleCompactCommandImpl?: CompactCommandHandler;
86
+ handleStopToolCommandImpl?: StopToolCommandHandler;
82
87
  loadCandidateAccountLoads?: (input: {
83
88
  sessionID: string;
84
89
  modelID?: string;
@@ -11,6 +11,7 @@ import { createNotifyTool } from "./notify-tool.js";
11
11
  import { createWaitTool } from "./wait-tool.js";
12
12
  import { refreshActiveAccountQuota } from "./active-account-quota.js";
13
13
  import { handleStatusCommand, showStatusToast } from "./status-command.js";
14
+ import { handleCompactCommand, handleStopToolCommand, } from "./session-control-command.js";
14
15
  import { appendRoutingEvent, appendRouteDecisionEvent, appendSessionTouchEvent, buildCandidateAccountLoads, isAccountRateLimitCooledDown, readRoutingState, routingStatePath, } from "./routing-state.js";
15
16
  const SESSION_BINDING_IDLE_TTL_MS = 30 * 60 * 1000;
16
17
  const RATE_LIMIT_WINDOW_MS = 5 * 60 * 1000;
@@ -364,6 +365,8 @@ export function buildPluginHooks(input) {
364
365
  };
365
366
  const refreshQuota = input.refreshQuota ?? ((store) => refreshActiveAccountQuota({ store }));
366
367
  const handleStatusCommandImpl = input.handleStatusCommandImpl ?? handleStatusCommand;
368
+ const handleCompactCommandImpl = input.handleCompactCommandImpl ?? handleCompactCommand;
369
+ const handleStopToolCommandImpl = input.handleStopToolCommandImpl ?? handleStopToolCommand;
367
370
  const loadOfficialConfig = input.loadOfficialConfig ?? loadOfficialCopilotConfig;
368
371
  const loadOfficialChatHeaders = input.loadOfficialChatHeaders ?? loadOfficialCopilotChatHeaders;
369
372
  const createRetryFetch = input.createRetryFetch ?? createCopilotRetryingFetch;
@@ -556,7 +559,7 @@ export function buildPluginHooks(input) {
556
559
  const initiator = getMergedRequestHeader(selectionRequest, selectionInit, "x-initiator");
557
560
  const candidates = latestStore ? resolveCopilotModelAccounts(latestStore, modelID) : [];
558
561
  if (candidates.length === 0) {
559
- const outbound = stripInternalSessionHeader(request, init);
562
+ const outbound = stripInternalSessionHeader(selectionRequest, selectionInit);
560
563
  return config.fetch(outbound.request, outbound.init);
561
564
  }
562
565
  const hasExistingBinding = sessionID.length > 0 && sessionAccountBindings.has(sessionID);
@@ -600,7 +603,7 @@ export function buildPluginHooks(input) {
600
603
  })
601
604
  : undefined;
602
605
  if (!resolved) {
603
- const outbound = stripInternalSessionHeader(request, init);
606
+ const outbound = stripInternalSessionHeader(selectionRequest, selectionInit);
604
607
  return config.fetch(outbound.request, outbound.init);
605
608
  }
606
609
  const candidateNames = candidates.map((item) => item.name);
@@ -639,13 +642,13 @@ export function buildPluginHooks(input) {
639
642
  if (isFirstUse) {
640
643
  modelAccountFirstUse.add(resolved.name);
641
644
  }
642
- let nextRequest = request;
643
- let nextInit = init;
644
- const currentInitiator = getMergedRequestHeader(request, init, "x-initiator");
645
+ let nextRequest = selectionRequest;
646
+ let nextInit = selectionInit;
647
+ const currentInitiator = getMergedRequestHeader(selectionRequest, selectionInit, "x-initiator");
645
648
  const shouldStripAgentInitiator = classification.reason === "unbound-fallback"
646
649
  || (isFirstUse && currentInitiator === "agent");
647
650
  if (shouldStripAgentInitiator && currentInitiator === "agent") {
648
- const rewritten = mergeAndRewriteRequestHeaders(request, init, (headers) => {
651
+ const rewritten = mergeAndRewriteRequestHeaders(selectionRequest, selectionInit, (headers) => {
649
652
  headers.delete("x-initiator");
650
653
  });
651
654
  nextRequest = rewritten.request;
@@ -1034,6 +1037,14 @@ export function buildPluginHooks(input) {
1034
1037
  template: "Show the current GitHub Copilot quota status via the experimental workaround path.",
1035
1038
  description: "Experimental Copilot quota status workaround",
1036
1039
  };
1040
+ config.command["copilot-compact"] = {
1041
+ template: "Summarize the current session via real session compacting flow.",
1042
+ description: "Experimental compact command for Copilot sessions",
1043
+ };
1044
+ config.command["copilot-stop-tool"] = {
1045
+ template: "Interrupt the current session tool flow, annotate the interrupted result, and append a synthetic continue.",
1046
+ description: "Experimental interrupt-and-annotate recovery with synthetic continue for Copilot sessions",
1047
+ };
1037
1048
  config.command["copilot-inject"] = {
1038
1049
  template: "Arm an immediate tool-output inject marker flow that drives model to question.",
1039
1050
  description: "Experimental force-intervene hook for Copilot workflows",
@@ -1072,6 +1083,25 @@ export function buildPluginHooks(input) {
1072
1083
  refreshQuota,
1073
1084
  });
1074
1085
  }
1086
+ if (hookInput.command === "copilot-compact") {
1087
+ if (!areExperimentalSlashCommandsEnabled(store))
1088
+ return;
1089
+ await handleCompactCommandImpl({
1090
+ client: input.client ?? {},
1091
+ sessionID: hookInput.sessionID,
1092
+ model: hookInput.model,
1093
+ });
1094
+ }
1095
+ if (hookInput.command === "copilot-stop-tool") {
1096
+ if (!areExperimentalSlashCommandsEnabled(store))
1097
+ return;
1098
+ await handleStopToolCommandImpl({
1099
+ client: input.client ?? {},
1100
+ sessionID: hookInput.sessionID,
1101
+ runningTools: hookInput.runningTools,
1102
+ syntheticAgentInitiatorEnabled: store?.syntheticAgentInitiatorEnabled === true,
1103
+ });
1104
+ }
1075
1105
  },
1076
1106
  "tool.execute.before": async (hookInput) => {
1077
1107
  if (!injectArmed)
@@ -0,0 +1,38 @@
1
+ type SessionToolPart = {
2
+ id?: unknown;
3
+ type?: unknown;
4
+ callID?: unknown;
5
+ state?: unknown;
6
+ output?: unknown;
7
+ error?: unknown;
8
+ };
9
+ export type SessionControlSessionToolPart = SessionToolPart;
10
+ export type SessionControlSessionPart = SessionToolPart;
11
+ export type SessionControlRunningTool = {
12
+ callID?: unknown;
13
+ tool?: unknown;
14
+ state?: unknown;
15
+ };
16
+ export type SessionControlToolContext = {
17
+ parts?: Array<SessionControlSessionPart>;
18
+ };
19
+ export type SessionControlToolInput = {
20
+ sessionID: string;
21
+ runningTools?: Array<SessionControlRunningTool>;
22
+ context?: SessionControlToolContext;
23
+ };
24
+ export declare class SessionControlCommandHandledError extends Error {
25
+ constructor();
26
+ }
27
+ export declare function handleCompactCommand(input: {
28
+ client?: unknown;
29
+ sessionID: string;
30
+ model?: string;
31
+ }): Promise<never>;
32
+ export declare function handleStopToolCommand(input: {
33
+ client?: unknown;
34
+ sessionID: string;
35
+ runningTools?: unknown;
36
+ syntheticAgentInitiatorEnabled?: boolean;
37
+ }): Promise<never>;
38
+ export {};
@@ -0,0 +1,434 @@
1
+ import { showStatusToast } from "./status-command.js";
2
+ export class SessionControlCommandHandledError extends Error {
3
+ constructor() {
4
+ super("session-control-command-handled");
5
+ this.name = "SessionControlCommandHandledError";
6
+ }
7
+ }
8
+ function warnToastFailure(scope, error) {
9
+ console.warn(`[${scope}] failed to show toast`, error);
10
+ }
11
+ async function showToast(input) {
12
+ await showStatusToast({
13
+ client: input.client,
14
+ message: input.message,
15
+ variant: input.variant,
16
+ warn: warnToastFailure,
17
+ });
18
+ }
19
+ async function getSessionMessages(session, sessionID) {
20
+ const messages = await session?.messages?.({
21
+ path: {
22
+ id: sessionID,
23
+ },
24
+ }).catch(() => undefined);
25
+ return Array.isArray(messages?.data) ? messages.data : [];
26
+ }
27
+ function getLatestAssistantModel(messages) {
28
+ for (const message of messages) {
29
+ if (message?.info?.role !== "assistant")
30
+ continue;
31
+ if (typeof message.model === "string" && message.model.length > 0)
32
+ return message.model;
33
+ }
34
+ for (const message of messages) {
35
+ if (typeof message.model === "string" && message.model.length > 0)
36
+ return message.model;
37
+ }
38
+ return undefined;
39
+ }
40
+ function normalizeRunningTools(runningTools) {
41
+ if (!Array.isArray(runningTools))
42
+ return [];
43
+ return runningTools.filter((item) => {
44
+ if (!item || typeof item !== "object")
45
+ return false;
46
+ const callID = item.callID;
47
+ return typeof callID === "string" && callID.length > 0;
48
+ }).map((item) => {
49
+ const normalized = item;
50
+ return {
51
+ callID: normalized.callID,
52
+ tool: normalized.tool,
53
+ state: normalized.state,
54
+ };
55
+ });
56
+ }
57
+ function normalizeToolState(state) {
58
+ if (state === "running" || state === "pending")
59
+ return "running";
60
+ if (typeof state === "string" && state.length > 0)
61
+ return "other";
62
+ return "unknown";
63
+ }
64
+ function getToolStateFromMessage(message, callID) {
65
+ const parts = Array.isArray(message.parts) ? message.parts : [];
66
+ for (const part of parts) {
67
+ if (part?.type !== "tool")
68
+ continue;
69
+ if (part?.callID !== callID)
70
+ continue;
71
+ if (part?.state === "running" || part?.state === "pending")
72
+ return "running";
73
+ return "done";
74
+ }
75
+ return "missing";
76
+ }
77
+ function findAssistantStateForCallID(input) {
78
+ const assistants = input.messages.filter((message) => message?.info?.role === "assistant");
79
+ if (input.targetAssistantID) {
80
+ const target = assistants.find((message) => message?.info?.id === input.targetAssistantID);
81
+ if (!target)
82
+ return "unknown";
83
+ const targetState = getToolStateFromMessage(target, input.callID);
84
+ if (targetState === "running")
85
+ return "running";
86
+ if (targetState === "done")
87
+ return "done";
88
+ return "unknown";
89
+ }
90
+ for (const assistant of assistants) {
91
+ const state = getToolStateFromMessage(assistant, input.callID);
92
+ if (state === "running")
93
+ return "running";
94
+ if (state === "done")
95
+ return "done";
96
+ }
97
+ return "unknown";
98
+ }
99
+ function resolveRunningCandidates(input) {
100
+ const running = [];
101
+ for (const callID of input.callIDs) {
102
+ for (const message of input.messages) {
103
+ if (message?.info?.role !== "assistant")
104
+ continue;
105
+ const state = getToolStateFromMessage(message, callID);
106
+ if (state !== "running")
107
+ continue;
108
+ running.push({
109
+ callID,
110
+ assistantID: typeof message.info?.id === "string" && message.info.id.length > 0
111
+ ? message.info.id
112
+ : undefined,
113
+ });
114
+ break;
115
+ }
116
+ }
117
+ return running;
118
+ }
119
+ async function waitForUniqueRunningCandidate(input) {
120
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
121
+ const maxAttempts = input.maxAttempts ?? 8;
122
+ const intervalMs = input.intervalMs ?? 30;
123
+ let sawMultiple = false;
124
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
125
+ const messages = await getSessionMessages(input.session, input.sessionID);
126
+ const candidates = resolveRunningCandidates({
127
+ messages,
128
+ callIDs: input.callIDs,
129
+ });
130
+ if (candidates.length === 1)
131
+ return {
132
+ type: "unique",
133
+ candidate: candidates[0],
134
+ };
135
+ if (candidates.length > 1)
136
+ sawMultiple = true;
137
+ if (attempt < maxAttempts - 1)
138
+ await sleep(intervalMs);
139
+ }
140
+ if (sawMultiple) {
141
+ return {
142
+ type: "multiple",
143
+ };
144
+ }
145
+ return {
146
+ type: "none",
147
+ };
148
+ }
149
+ function findRecentAssistantByCallID(input) {
150
+ for (const message of input.messages) {
151
+ if (message?.info?.role !== "assistant")
152
+ continue;
153
+ if (getToolStateFromMessage(message, input.callID) === "missing")
154
+ continue;
155
+ const id = message.info?.id;
156
+ if (typeof id === "string" && id.length > 0)
157
+ return id;
158
+ return undefined;
159
+ }
160
+ return undefined;
161
+ }
162
+ async function waitForToolToStop(input) {
163
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
164
+ const maxAttempts = input.maxAttempts ?? 30;
165
+ const intervalMs = input.intervalMs ?? 30;
166
+ let resolvedTargetAssistantID = input.targetAssistantID;
167
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
168
+ const messages = await getSessionMessages(input.session, input.sessionID);
169
+ const assistants = messages.filter((message) => message?.info?.role === "assistant");
170
+ if (!resolvedTargetAssistantID) {
171
+ resolvedTargetAssistantID = findRecentAssistantByCallID({
172
+ messages,
173
+ callID: input.callID,
174
+ });
175
+ }
176
+ const targets = resolvedTargetAssistantID
177
+ ? assistants.filter((message) => message?.info?.id === resolvedTargetAssistantID)
178
+ : assistants;
179
+ for (const message of targets) {
180
+ const parts = Array.isArray(message.parts) ? message.parts : [];
181
+ for (const part of parts) {
182
+ if (part?.type !== "tool")
183
+ continue;
184
+ if (part?.callID !== input.callID)
185
+ continue;
186
+ if (part?.state === "completed" || part?.state === "error") {
187
+ return {
188
+ type: "settled",
189
+ messageID: typeof message.info?.id === "string" ? message.info.id : undefined,
190
+ part,
191
+ };
192
+ }
193
+ }
194
+ }
195
+ const state = findAssistantStateForCallID({
196
+ messages,
197
+ callID: input.callID,
198
+ targetAssistantID: resolvedTargetAssistantID,
199
+ });
200
+ if (state === "done") {
201
+ return {
202
+ type: "done-without-part",
203
+ };
204
+ }
205
+ if (attempt < maxAttempts - 1)
206
+ await sleep(intervalMs);
207
+ }
208
+ return {
209
+ type: "timeout",
210
+ };
211
+ }
212
+ function appendInterruptedNote(base) {
213
+ const note = "用户主动中止,结果可能不完整。Interrupted by user; result may be incomplete.";
214
+ return `${base}${base.length > 0 ? "\n\n" : ""}${note}`;
215
+ }
216
+ async function patchStoppedToolTranscript(input) {
217
+ const partID = typeof input.part.id === "string" ? input.part.id : undefined;
218
+ const partState = input.part.state;
219
+ let patch;
220
+ if (partState === "completed") {
221
+ const output = typeof input.part.output === "string" ? input.part.output : "";
222
+ patch = {
223
+ output: appendInterruptedNote(output),
224
+ };
225
+ }
226
+ else if (partState === "error") {
227
+ const error = typeof input.part.error === "string" ? input.part.error : "";
228
+ patch = {
229
+ error: appendInterruptedNote(error),
230
+ };
231
+ }
232
+ else {
233
+ throw new Error("tool part is not in completed/error state");
234
+ }
235
+ const partClient = input.client?.part;
236
+ if (!partClient?.update || !input.messageID || !partID) {
237
+ throw new Error("client.part.update unavailable");
238
+ }
239
+ await partClient.update({
240
+ sessionID: input.sessionID,
241
+ messageID: input.messageID,
242
+ partID,
243
+ part: patch,
244
+ });
245
+ }
246
+ export async function handleCompactCommand(input) {
247
+ const session = input.client?.session;
248
+ const summarize = session?.summarize;
249
+ const model = input.model ?? getLatestAssistantModel(await getSessionMessages(session, input.sessionID));
250
+ if (!model) {
251
+ await showToast({
252
+ client: input.client,
253
+ message: "No assistant model context available for compact.",
254
+ variant: "warning",
255
+ });
256
+ throw new SessionControlCommandHandledError();
257
+ }
258
+ if (summarize) {
259
+ await summarize({
260
+ auto: true,
261
+ model,
262
+ });
263
+ throw new SessionControlCommandHandledError();
264
+ }
265
+ await showToast({
266
+ client: input.client,
267
+ message: "Session summarize is unavailable for compact.",
268
+ variant: "warning",
269
+ });
270
+ throw new SessionControlCommandHandledError();
271
+ }
272
+ export async function handleStopToolCommand(input) {
273
+ if (input.syntheticAgentInitiatorEnabled !== true) {
274
+ await showToast({
275
+ client: input.client,
276
+ message: "Enable 'Send synthetic messages as agent' first; otherwise this recovery adds one extra billed synthetic turn.",
277
+ variant: "warning",
278
+ });
279
+ throw new SessionControlCommandHandledError();
280
+ }
281
+ const runningTools = normalizeRunningTools(input.runningTools);
282
+ if (runningTools.length === 0) {
283
+ await showToast({
284
+ client: input.client,
285
+ message: "No running tool found.",
286
+ variant: "warning",
287
+ });
288
+ throw new SessionControlCommandHandledError();
289
+ }
290
+ const runningPendingTools = runningTools.filter((item) => normalizeToolState(item.state) === "running");
291
+ const hasExplicitToolState = runningTools.some((item) => normalizeToolState(item.state) !== "unknown");
292
+ if (hasExplicitToolState && runningPendingTools.length === 0) {
293
+ await showToast({
294
+ client: input.client,
295
+ message: "No running/pending tool found.",
296
+ variant: "warning",
297
+ });
298
+ throw new SessionControlCommandHandledError();
299
+ }
300
+ if (hasExplicitToolState && runningPendingTools.length > 1) {
301
+ await showToast({
302
+ client: input.client,
303
+ message: "Found multiple running/pending tools; abort a single tool only.",
304
+ variant: "warning",
305
+ });
306
+ throw new SessionControlCommandHandledError();
307
+ }
308
+ if (!hasExplicitToolState && runningTools.length > 1) {
309
+ await showToast({
310
+ client: input.client,
311
+ message: "Found multiple running tools; abort a single tool only.",
312
+ variant: "warning",
313
+ });
314
+ throw new SessionControlCommandHandledError();
315
+ }
316
+ const session = input.client?.session;
317
+ const sessionCandidate = hasExplicitToolState && runningPendingTools.length === 1
318
+ ? {
319
+ type: "unique",
320
+ candidate: {
321
+ callID: runningPendingTools[0].callID,
322
+ assistantID: undefined,
323
+ },
324
+ }
325
+ : await waitForUniqueRunningCandidate({
326
+ session,
327
+ sessionID: input.sessionID,
328
+ callIDs: [...new Set(runningTools.map((item) => item.callID))],
329
+ });
330
+ if (!session?.abort) {
331
+ await showToast({
332
+ client: input.client,
333
+ message: "Stop-tool abort unavailable: session.abort capability is missing.",
334
+ variant: "error",
335
+ });
336
+ throw new SessionControlCommandHandledError();
337
+ }
338
+ if (sessionCandidate.type === "none") {
339
+ await showToast({
340
+ client: input.client,
341
+ message: "No running/pending tool found.",
342
+ variant: "warning",
343
+ });
344
+ throw new SessionControlCommandHandledError();
345
+ }
346
+ if (sessionCandidate.type === "multiple") {
347
+ await showToast({
348
+ client: input.client,
349
+ message: "Found multiple running/pending tools; abort a single tool only.",
350
+ variant: "warning",
351
+ });
352
+ throw new SessionControlCommandHandledError();
353
+ }
354
+ const callID = sessionCandidate.candidate.callID;
355
+ const targetAssistantID = sessionCandidate.candidate.assistantID;
356
+ try {
357
+ await session.abort({
358
+ path: {
359
+ id: input.sessionID,
360
+ },
361
+ callID,
362
+ });
363
+ }
364
+ catch (error) {
365
+ const message = error instanceof Error ? error.message : String(error);
366
+ await showToast({
367
+ client: input.client,
368
+ message: `Stop-tool abort failed: ${message}`,
369
+ variant: "error",
370
+ });
371
+ throw new SessionControlCommandHandledError();
372
+ }
373
+ const settled = await waitForToolToStop({
374
+ session,
375
+ sessionID: input.sessionID,
376
+ callID,
377
+ targetAssistantID,
378
+ });
379
+ if (settled.type === "timeout") {
380
+ await showToast({
381
+ client: input.client,
382
+ message: "Stop-tool failed: tool part state remained unstable and hit timeout.",
383
+ variant: "error",
384
+ });
385
+ throw new SessionControlCommandHandledError();
386
+ }
387
+ if (settled.type !== "settled") {
388
+ await showToast({
389
+ client: input.client,
390
+ message: "Stop-tool failed: tool part was not available for transcript patch.",
391
+ variant: "error",
392
+ });
393
+ throw new SessionControlCommandHandledError();
394
+ }
395
+ try {
396
+ await patchStoppedToolTranscript({
397
+ client: input.client,
398
+ sessionID: input.sessionID,
399
+ messageID: settled.messageID,
400
+ part: settled.part,
401
+ });
402
+ }
403
+ catch (error) {
404
+ const message = error instanceof Error ? error.message : String(error);
405
+ await showToast({
406
+ client: input.client,
407
+ message: `Stop-tool transcript patch failed: ${message}`,
408
+ variant: "error",
409
+ });
410
+ throw new SessionControlCommandHandledError();
411
+ }
412
+ try {
413
+ if (!session?.promptAsync) {
414
+ throw new Error("promptAsync unavailable");
415
+ }
416
+ await session.promptAsync({
417
+ sessionID: input.sessionID,
418
+ synthetic: true,
419
+ parts: [{
420
+ type: "text",
421
+ text: "The previous tool call was interrupted at the user's request. Treat its result as partial evidence. Do not resume that same tool call automatically unless the user explicitly asks for it.",
422
+ }],
423
+ });
424
+ }
425
+ catch (error) {
426
+ const message = error instanceof Error ? error.message : String(error);
427
+ await showToast({
428
+ client: input.client,
429
+ message: `Stop-tool recovery failed: ${message}`,
430
+ variant: "error",
431
+ });
432
+ }
433
+ throw new SessionControlCommandHandledError();
434
+ }
@@ -24,12 +24,12 @@ function truncateMiddle(value, maxWidth) {
24
24
  return "";
25
25
  if (value.length <= maxWidth)
26
26
  return value;
27
- if (maxWidth <= 3)
28
- return ".".repeat(maxWidth);
29
- const visibleWidth = maxWidth - 3;
27
+ if (maxWidth === 1)
28
+ return "";
29
+ const visibleWidth = maxWidth - 1;
30
30
  const leftWidth = Math.ceil(visibleWidth / 2);
31
31
  const rightWidth = Math.floor(visibleWidth / 2);
32
- return `${value.slice(0, leftWidth)}...${value.slice(value.length - rightWidth)}`;
32
+ return `${value.slice(0, leftWidth)}…${value.slice(value.length - rightWidth)}`;
33
33
  }
34
34
  function renderAccountCell(name, quotaText) {
35
35
  if (quotaText.length >= ACCOUNT_CELL_WIDTH) {
@@ -59,25 +59,6 @@ function renderAccountGrid(cells) {
59
59
  }
60
60
  return rows;
61
61
  }
62
- function formatActiveGroup(store) {
63
- const names = Array.isArray(store.activeAccountNames) ? store.activeAccountNames : [];
64
- if (names.length > 0)
65
- return names.join(", ");
66
- return "none";
67
- }
68
- function formatRoutingGroup(store) {
69
- const assignments = store.modelAccountAssignments ?? {};
70
- const modelIDs = Object.keys(assignments).sort((a, b) => a.localeCompare(b));
71
- const mapped = modelIDs
72
- .map((modelID) => {
73
- const names = assignments[modelID] ?? [];
74
- if (names.length === 0)
75
- return undefined;
76
- return `${modelID} -> ${names.join(", ")}`;
77
- })
78
- .filter((line) => Boolean(line));
79
- return mapped.length > 0 ? mapped.join("; ") : "none";
80
- }
81
62
  function buildSuccessMessage(store, _name) {
82
63
  const defaultNames = Array.isArray(store.activeAccountNames) && store.activeAccountNames.length > 0
83
64
  ? store.activeAccountNames
@@ -110,8 +91,6 @@ function buildSuccessMessage(store, _name) {
110
91
  lines.push("[routes]");
111
92
  lines.push("(none)");
112
93
  }
113
- lines.push(`活跃组: ${formatActiveGroup(store)}`);
114
- lines.push(`路由组: ${formatRoutingGroup(store)}`);
115
94
  return lines.join("\n");
116
95
  }
117
96
  function buildMissingActiveMessage() {
package/dist/ui/menu.js CHANGED
@@ -27,7 +27,7 @@ export function getMenuCopy(language = "zh") {
27
27
  policyScopeHint: "Choose whether Guided Loop Safety applies only to Copilot by default or to all models",
28
28
  experimentalSlashCommandsOn: "Experimental slash commands: On",
29
29
  experimentalSlashCommandsOff: "Experimental slash commands: Off",
30
- experimentalSlashCommandsHint: "Controls whether /copilot-status, /copilot-inject, and /copilot-policy-all-models are registered",
30
+ experimentalSlashCommandsHint: "Controls whether /copilot-status, /copilot-compact, /copilot-stop-tool, /copilot-inject, and /copilot-policy-all-models are registered",
31
31
  retryOn: "Copilot Network Retry: On",
32
32
  retryOff: "Copilot Network Retry: Off",
33
33
  retryHint: "Helps recover some requests after account switches or malformed retries",
@@ -63,7 +63,7 @@ export function getMenuCopy(language = "zh") {
63
63
  policyScopeHint: "决定 Guided Loop Safety 默认只作用于 Copilot,还是扩展到所有模型",
64
64
  experimentalSlashCommandsOn: "实验性 Slash Commands:已开启",
65
65
  experimentalSlashCommandsOff: "实验性 Slash Commands:已关闭",
66
- experimentalSlashCommandsHint: "控制 /copilot-status、/copilot-inject、/copilot-policy-all-models 是否注册",
66
+ experimentalSlashCommandsHint: "控制 /copilot-status、/copilot-compact、/copilot-stop-tool、/copilot-inject、/copilot-policy-all-models 是否注册",
67
67
  retryOn: "Copilot Network Retry:已开启",
68
68
  retryOff: "Copilot Network Retry:已关闭",
69
69
  retryHint: "账号切换后若出现请求异常,可自动重试并修复部分请求",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-copilot-account-switcher",
3
- "version": "0.10.12",
3
+ "version": "0.11.1",
4
4
  "description": "GitHub Copilot account switcher plugin for OpenCode",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",