@mem9/opencode 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/opencode",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "OpenCode memory plugin — cloud-persistent AI agent memory with hybrid vector + keyword search via mem9",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -3,6 +3,7 @@ import type { IngestMessage, MemoryBackend } from "./backend.ts";
3
3
  import type { DebugLogger } from "./debug.ts";
4
4
  import { selectMessagesForIngest } from "./ingest/select.ts";
5
5
  import { submitMessagesForIngest } from "./ingest/submit.ts";
6
+ import { formatRuntimeQuotaNotice, parseRuntimeQuotaDenied } from "./quota-error.ts";
6
7
  import { formatRecallBlock } from "./recall/format.ts";
7
8
  import { buildRecallQuery } from "./recall/query.ts";
8
9
  import type { SessionTranscriptLoader } from "./session-transcript.ts";
@@ -185,14 +186,16 @@ async function ingestSessionTranscript(
185
186
  sessionID,
186
187
  messageCount: selectedMessages.length,
187
188
  });
188
- await submitMessagesForIngest({
189
+ const result = await submitMessagesForIngest({
189
190
  backend,
190
191
  messages: transcript,
191
192
  sessionID,
192
193
  agentID: state.agentID,
193
194
  debugLogger: options.debugLogger,
194
195
  });
195
- state.lastIngestFingerprint = fingerprint;
196
+ if (result) {
197
+ state.lastIngestFingerprint = fingerprint;
198
+ }
196
199
  } finally {
197
200
  if (state.pendingIngestFingerprint === fingerprint) {
198
201
  state.pendingIngestFingerprint = null;
@@ -308,6 +311,18 @@ export function buildHooks(
308
311
  output.system.push(block);
309
312
  }
310
313
  } catch (error) {
314
+ const quotaDenied = parseRuntimeQuotaDenied(error);
315
+ if (quotaDenied) {
316
+ await options.debugLogger?.("recall.quota_denied", {
317
+ sessionID: input.sessionID,
318
+ code: quotaDenied.code,
319
+ actionType: quotaDenied.recommendedAction?.type,
320
+ hasActionUrl: Boolean(quotaDenied.recommendedAction?.url),
321
+ });
322
+ output.system.push(formatRuntimeQuotaNotice(error, "recall paused"));
323
+ return;
324
+ }
325
+
311
326
  await options.debugLogger?.("recall.error", {
312
327
  sessionID: input.sessionID,
313
328
  error: error instanceof Error ? error.message : String(error),
@@ -1,5 +1,6 @@
1
1
  import type { IngestInput, IngestResult, MemoryBackend } from "../backend.ts";
2
2
  import type { DebugLogger } from "../debug.ts";
3
+ import { parseRuntimeQuotaDenied } from "../quota-error.ts";
3
4
  import { selectMessagesForIngest } from "./select.ts";
4
5
 
5
6
  export interface SubmitIngestOptions {
@@ -45,6 +46,18 @@ export async function submitMessagesForIngest(
45
46
  });
46
47
  return result;
47
48
  } catch (error) {
49
+ const quotaDenied = parseRuntimeQuotaDenied(error);
50
+ if (quotaDenied) {
51
+ await options.debugLogger?.("ingest.quota_denied", {
52
+ sessionID: options.sessionID,
53
+ agentID: options.agentID,
54
+ code: quotaDenied.code,
55
+ actionType: quotaDenied.recommendedAction?.type,
56
+ hasActionUrl: Boolean(quotaDenied.recommendedAction?.url),
57
+ });
58
+ return null;
59
+ }
60
+
48
61
  await options.debugLogger?.("ingest.error", {
49
62
  sessionID: options.sessionID,
50
63
  agentID: options.agentID,
@@ -0,0 +1,259 @@
1
+ export class Mem9HttpError extends Error {
2
+ constructor(
3
+ message: string,
4
+ readonly status: number,
5
+ readonly body: string,
6
+ readonly data: unknown,
7
+ ) {
8
+ super(message);
9
+ this.name = "Mem9HttpError";
10
+ }
11
+ }
12
+
13
+ export interface RuntimeRecommendedAction {
14
+ providerActionCode?: string;
15
+ severity?: string;
16
+ type?: string;
17
+ url?: string;
18
+ }
19
+
20
+ export interface RuntimeQuotaDenied {
21
+ status: number | null;
22
+ code: string;
23
+ message: string;
24
+ meter?: string;
25
+ quotaGateReason?: string;
26
+ retryAfterSeconds?: number;
27
+ recommendedAction?: RuntimeRecommendedAction;
28
+ }
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ function normalizeString(value: unknown): string {
35
+ return typeof value === "string" ? value.trim() : "";
36
+ }
37
+
38
+ function normalizePositiveInteger(value: unknown): number | null {
39
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
40
+ }
41
+
42
+ export function parseJsonOrUndefined(text: string): unknown {
43
+ if (!text.trim()) {
44
+ return undefined;
45
+ }
46
+
47
+ try {
48
+ return JSON.parse(text) as unknown;
49
+ } catch {
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ export function messageFromErrorBody(status: number, body: string, data: unknown): string {
55
+ if (isRecord(data)) {
56
+ const message = normalizeString(data.message);
57
+ if (message) {
58
+ return message;
59
+ }
60
+
61
+ const error = normalizeString(data.error);
62
+ if (error) {
63
+ return error;
64
+ }
65
+ }
66
+
67
+ const text = body.trim();
68
+ return text || `HTTP ${status}`;
69
+ }
70
+
71
+ function normalizeRecommendedAction(runtimeQuota: Record<string, unknown>): RuntimeRecommendedAction | null {
72
+ const nested = isRecord(runtimeQuota.recommendedAction) ? runtimeQuota.recommendedAction : {};
73
+ const providerActionCode = normalizeString(nested.providerActionCode);
74
+ const severity = normalizeString(nested.severity);
75
+ const type = normalizeString(nested.type);
76
+ const url = normalizeString(nested.url);
77
+
78
+ if (!providerActionCode && !severity && !type && !url) {
79
+ return null;
80
+ }
81
+
82
+ return {
83
+ ...(providerActionCode ? { providerActionCode } : {}),
84
+ ...(severity ? { severity } : {}),
85
+ ...(type ? { type } : {}),
86
+ ...(url ? { url } : {}),
87
+ };
88
+ }
89
+
90
+ function quotaGateReason(runtimeQuota: Record<string, unknown>): string {
91
+ const quotaGateResult = isRecord(runtimeQuota.quotaGateResult) ? runtimeQuota.quotaGateResult : {};
92
+ return normalizeString(quotaGateResult.reason);
93
+ }
94
+
95
+ function retryAfterSeconds(runtimeQuota: Record<string, unknown>): number | null {
96
+ const direct = normalizePositiveInteger(runtimeQuota.retryAfterSeconds);
97
+ if (direct !== null) {
98
+ return direct;
99
+ }
100
+
101
+ const quotaGateResult = isRecord(runtimeQuota.quotaGateResult) ? runtimeQuota.quotaGateResult : {};
102
+ const postQuotaRateLimit = isRecord(quotaGateResult.postQuotaRateLimit) ? quotaGateResult.postQuotaRateLimit : {};
103
+ return normalizePositiveInteger(postQuotaRateLimit.retryAfterSeconds);
104
+ }
105
+
106
+ function isPostQuotaRateLimited(denied: RuntimeQuotaDenied): boolean {
107
+ return denied.status === 429 ||
108
+ denied.quotaGateReason === "postQuotaRateLimitExceeded";
109
+ }
110
+
111
+ function quotaReason(denied: RuntimeQuotaDenied): string {
112
+ if (isPostQuotaRateLimited(denied)) {
113
+ return "this API key has reached the temporary request limit for this memory feature";
114
+ }
115
+ const providerActionCode = normalizeString(denied.recommendedAction?.providerActionCode);
116
+ if (providerActionCode === "claimApiKey") {
117
+ return "the included usage quota for this API key has been used up";
118
+ }
119
+ if (providerActionCode === "increaseSpendingLimit") {
120
+ return "the configured spending limit would be exceeded";
121
+ }
122
+ if (providerActionCode === "enableOnDemand") {
123
+ return "the included usage quota has been used up and on-demand usage is not enabled";
124
+ }
125
+ if (providerActionCode === "upgradePlan") {
126
+ return "the included usage quota for this mem9 account has been used up";
127
+ }
128
+ if (providerActionCode === "resolveAccountState") {
129
+ return "the current account or billing state blocks runtime memory access";
130
+ }
131
+ return "the runtime quota check blocked this request";
132
+ }
133
+
134
+ function quotaNoticeSubject(denied: RuntimeQuotaDenied, operation: string): { headline: string; userState: string } {
135
+ const meter = normalizeString(denied.meter);
136
+ if (meter === "memory_write_requests") {
137
+ return {
138
+ headline: "Mem9 memory saving is temporarily unavailable",
139
+ userState: "mem9 cannot save new memories right now",
140
+ };
141
+ }
142
+ if (meter === "memory_recall_requests") {
143
+ return {
144
+ headline: "Mem9 recall is temporarily unavailable",
145
+ userState: "mem9 cannot recall memories right now",
146
+ };
147
+ }
148
+
149
+ const operationText = normalizeString(operation).toLowerCase();
150
+ if (/\b(ingest|save|store|write)\b/.test(operationText)) {
151
+ return {
152
+ headline: "Mem9 memory saving is temporarily unavailable",
153
+ userState: "mem9 cannot save new memories right now",
154
+ };
155
+ }
156
+ if (/\b(recall|search)\b/.test(operationText)) {
157
+ return {
158
+ headline: "Mem9 recall is temporarily unavailable",
159
+ userState: "mem9 cannot recall memories right now",
160
+ };
161
+ }
162
+
163
+ return {
164
+ headline: "Mem9 memory is temporarily unavailable",
165
+ userState: "mem9 cannot complete the memory request right now",
166
+ };
167
+ }
168
+
169
+ function actionUrlForDenied(denied: RuntimeQuotaDenied): string {
170
+ return normalizeString(denied.recommendedAction?.url);
171
+ }
172
+
173
+ function actionInstruction(denied: RuntimeQuotaDenied): string {
174
+ const action = denied.recommendedAction;
175
+ const providerActionCode = normalizeString(action?.providerActionCode);
176
+ const actionUrl = actionUrlForDenied(denied);
177
+ if (!actionUrl) {
178
+ if (isPostQuotaRateLimited(denied)) {
179
+ return "Tell them that the quota/rate-limit check blocked this request and to retry later or open the mem9 console to review account and billing settings.";
180
+ }
181
+ return "Ask them to open the mem9 console to resolve the account or billing state.";
182
+ }
183
+
184
+ switch (providerActionCode) {
185
+ case "claimApiKey":
186
+ return `Ask them to open this link to sign in or create a mem9 account and claim this API key: ${actionUrl}. After claiming the key, they can upgrade their plan or set up billing to get more usage. Include the link exactly as written.`;
187
+ case "upgradePlan":
188
+ return `Ask them to open this link to upgrade their mem9 plan and get more included usage: ${actionUrl}. Include the link exactly as written.`;
189
+ case "increaseSpendingLimit":
190
+ return `Ask them to open this link to increase the mem9 spending limit or adjust billing settings: ${actionUrl}. Include the link exactly as written.`;
191
+ case "enableOnDemand":
192
+ return `Ask them to open this link to enable billing or on-demand usage for mem9: ${actionUrl}. Include the link exactly as written.`;
193
+ case "resolveAccountState":
194
+ return `Ask them to open this mem9 link to resolve the account or billing state: ${actionUrl}. Include the link exactly as written.`;
195
+ default:
196
+ return `Ask them to open this mem9 link to resolve the account or billing state: ${actionUrl}. Include the link exactly as written.`;
197
+ }
198
+ }
199
+
200
+ export function parseRuntimeQuotaDenied(value: unknown): RuntimeQuotaDenied | null {
201
+ const payload = value instanceof Mem9HttpError ? value.data : value;
202
+ if (!isRecord(payload)) {
203
+ return null;
204
+ }
205
+
206
+ const details = isRecord(payload.details) ? payload.details : {};
207
+ if (normalizeString(details.errorCategory) !== "runtime_quota_denied") {
208
+ return null;
209
+ }
210
+ const runtimeQuota = isRecord(details.runtimeQuota) ? details.runtimeQuota : {};
211
+
212
+ const recommendedAction = normalizeRecommendedAction(runtimeQuota);
213
+ const retryAfter = retryAfterSeconds(runtimeQuota);
214
+ const reason = quotaGateReason(runtimeQuota);
215
+ return {
216
+ status: value instanceof Mem9HttpError ? value.status : null,
217
+ code: "runtime_quota_denied",
218
+ message: normalizeString(payload.error) || "Runtime usage quota denied.",
219
+ ...(normalizeString(runtimeQuota.meter) ? { meter: normalizeString(runtimeQuota.meter) } : {}),
220
+ ...(reason ? { quotaGateReason: reason } : {}),
221
+ ...(retryAfter !== null ? { retryAfterSeconds: retryAfter } : {}),
222
+ ...(recommendedAction ? { recommendedAction } : {}),
223
+ };
224
+ }
225
+
226
+ export function formatRuntimeQuotaNotice(value: unknown, operation: string): string {
227
+ const denied = parseRuntimeQuotaDenied(value);
228
+ if (!denied) {
229
+ return "";
230
+ }
231
+
232
+ const subject = quotaNoticeSubject(denied, operation);
233
+ return `${subject.headline} because ${quotaReason(denied)}. In your reply, briefly tell the user that ${subject.userState}. ${actionInstruction(denied)}`;
234
+ }
235
+
236
+ export function toolErrorPayload(error: unknown): Record<string, unknown> {
237
+ const denied = parseRuntimeQuotaDenied(error);
238
+ if (denied) {
239
+ const actionUrl = actionUrlForDenied(denied);
240
+ return {
241
+ ok: false,
242
+ error: denied.message,
243
+ status_code: denied.status,
244
+ code: denied.code,
245
+ quota: {
246
+ code: denied.code,
247
+ message: denied.message,
248
+ ...(denied.retryAfterSeconds !== undefined ? { retryAfterSeconds: denied.retryAfterSeconds } : {}),
249
+ ...(denied.recommendedAction ? { recommendedAction: denied.recommendedAction } : {}),
250
+ },
251
+ ...(actionUrl ? { action_url: actionUrl } : {}),
252
+ };
253
+ }
254
+
255
+ return {
256
+ ok: false,
257
+ error: error instanceof Error ? error.message : String(error),
258
+ };
259
+ }
@@ -12,10 +12,11 @@ import type {
12
12
  SearchInput,
13
13
  } from "../shared/types.ts";
14
14
  import { DEFAULT_SCOPE_CONFIG } from "../shared/defaults.ts";
15
-
16
- function isRecord(value: unknown): value is Record<string, unknown> {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
18
- }
15
+ import {
16
+ Mem9HttpError,
17
+ messageFromErrorBody,
18
+ parseJsonOrUndefined,
19
+ } from "./quota-error.ts";
19
20
 
20
21
  function normalizeTimeoutMs(value: number | undefined, fallback: number): number {
21
22
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -82,13 +83,16 @@ export class ServerBackend implements MemoryBackend {
82
83
  if (resp.status === 204) return undefined as T;
83
84
 
84
85
  const text = await resp.text();
85
- const data = text ? (JSON.parse(text) as unknown) : undefined;
86
86
  if (!resp.ok) {
87
- const message =
88
- isRecord(data) && typeof data.error === "string" ? data.error : `HTTP ${resp.status}`;
89
- throw new Error(message);
87
+ const data = parseJsonOrUndefined(text);
88
+ throw new Mem9HttpError(
89
+ messageFromErrorBody(resp.status, text, data),
90
+ resp.status,
91
+ text,
92
+ data,
93
+ );
90
94
  }
91
- return data as T;
95
+ return JSON.parse(text) as T;
92
96
  }
93
97
 
94
98
  async store(input: CreateMemoryInput): Promise<StoreResult> {
@@ -133,7 +137,7 @@ export class ServerBackend implements MemoryBackend {
133
137
  try {
134
138
  return await this.request<Memory>("GET", this.memoryPath(`/memories/${id}`));
135
139
  } catch (err) {
136
- if (err instanceof Error && (err.message.includes("not found") || err.message.includes("404"))) {
140
+ if (err instanceof Mem9HttpError && err.status === 404) {
137
141
  return null;
138
142
  }
139
143
  throw err;
@@ -144,7 +148,7 @@ export class ServerBackend implements MemoryBackend {
144
148
  try {
145
149
  return await this.request<Memory>("PUT", this.memoryPath(`/memories/${id}`), input);
146
150
  } catch (err) {
147
- if (err instanceof Error && (err.message.includes("not found") || err.message.includes("404"))) {
151
+ if (err instanceof Mem9HttpError && err.status === 404) {
148
152
  return null;
149
153
  }
150
154
  throw err;
@@ -156,7 +160,7 @@ export class ServerBackend implements MemoryBackend {
156
160
  await this.request("DELETE", this.memoryPath(`/memories/${id}`));
157
161
  return true;
158
162
  } catch (err) {
159
- if (err instanceof Error && (err.message.includes("not found") || err.message.includes("404"))) {
163
+ if (err instanceof Mem9HttpError && err.status === 404) {
160
164
  return false;
161
165
  }
162
166
  throw err;
@@ -5,6 +5,11 @@ import type {
5
5
  UpdateMemoryInput,
6
6
  SearchInput,
7
7
  } from "../shared/types.ts";
8
+ import { toolErrorPayload } from "./quota-error.ts";
9
+
10
+ function jsonToolError(error: unknown): string {
11
+ return JSON.stringify(toolErrorPayload(error));
12
+ }
8
13
 
9
14
  /**
10
15
  * Build the 5 memory tools for OpenCode.
@@ -45,10 +50,7 @@ export function buildTools(backend: MemoryBackend): Record<string, ReturnType<ty
45
50
  const result = await backend.store(input);
46
51
  return JSON.stringify({ ok: true, data: result });
47
52
  } catch (err) {
48
- return JSON.stringify({
49
- ok: false,
50
- error: err instanceof Error ? err.message : String(err),
51
- });
53
+ return jsonToolError(err);
52
54
  }
53
55
  },
54
56
  }),
@@ -97,10 +99,7 @@ export function buildTools(backend: MemoryBackend): Record<string, ReturnType<ty
97
99
  const result = await backend.search(input);
98
100
  return JSON.stringify({ ok: true, ...result });
99
101
  } catch (err) {
100
- return JSON.stringify({
101
- ok: false,
102
- error: err instanceof Error ? err.message : String(err),
103
- });
102
+ return jsonToolError(err);
104
103
  }
105
104
  },
106
105
  }),
@@ -118,10 +117,7 @@ export function buildTools(backend: MemoryBackend): Record<string, ReturnType<ty
118
117
  }
119
118
  return JSON.stringify({ ok: true, data: result });
120
119
  } catch (err) {
121
- return JSON.stringify({
122
- ok: false,
123
- error: err instanceof Error ? err.message : String(err),
124
- });
120
+ return jsonToolError(err);
125
121
  }
126
122
  },
127
123
  }),
@@ -157,10 +153,7 @@ export function buildTools(backend: MemoryBackend): Record<string, ReturnType<ty
157
153
  }
158
154
  return JSON.stringify({ ok: true, data: result });
159
155
  } catch (err) {
160
- return JSON.stringify({
161
- ok: false,
162
- error: err instanceof Error ? err.message : String(err),
163
- });
156
+ return jsonToolError(err);
164
157
  }
165
158
  },
166
159
  }),
@@ -178,10 +171,7 @@ export function buildTools(backend: MemoryBackend): Record<string, ReturnType<ty
178
171
  }
179
172
  return JSON.stringify({ ok: true });
180
173
  } catch (err) {
181
- return JSON.stringify({
182
- ok: false,
183
- error: err instanceof Error ? err.message : String(err),
184
- });
174
+ return jsonToolError(err);
185
175
  }
186
176
  },
187
177
  }),
package/src/tui/index.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  TuiCommand,
4
4
  TuiPlugin,
5
5
  TuiPluginApi,
6
+ TuiToast,
6
7
  } from "@opencode-ai/plugin/tui";
7
8
  import {
8
9
  resolveMem9Home,
@@ -61,6 +62,22 @@ interface SetupProfileOptionState {
61
62
  currentProfileId?: string;
62
63
  }
63
64
 
65
+ let hasShownVisibleApiKeyWarning = false;
66
+ const DEFAULT_TOAST_DURATION_MS = 5000;
67
+
68
+ function scheduleDialogTransition(next: () => void): void {
69
+ // Delay prompt-to-prompt transitions so the next dialog does not reuse
70
+ // the same Enter keypress that confirmed the current prompt.
71
+ setTimeout(next, 0);
72
+ }
73
+
74
+ function showToast(api: TuiPluginApi, toast: TuiToast): void {
75
+ api.ui.toast({
76
+ duration: toast.duration ?? DEFAULT_TOAST_DURATION_MS,
77
+ ...toast,
78
+ });
79
+ }
80
+
64
81
  function getProjectDir(api: TuiPluginApi): string {
65
82
  const worktree = api.state.path.worktree.trim();
66
83
  if (worktree.length > 0) {
@@ -113,7 +130,7 @@ function showError(
113
130
  retryCommand = false,
114
131
  ): void {
115
132
  const suffix = retryCommand ? " Run /mem9-setup again when you are ready to retry." : "";
116
- api.ui.toast({
133
+ showToast(api, {
117
134
  variant: "error",
118
135
  title: "mem9 setup failed",
119
136
  message: `${error instanceof Error ? error.message : String(error)}${suffix}`,
@@ -121,7 +138,7 @@ function showError(
121
138
  }
122
139
 
123
140
  function showProfileSavedSuccess(api: TuiPluginApi, profileId: string): void {
124
- api.ui.toast({
141
+ showToast(api, {
125
142
  variant: "success",
126
143
  title: "mem9 configured",
127
144
  message: `Saved profile ${profileId} and set it as the default user profile. Restart OpenCode to reload mem9.`,
@@ -134,7 +151,7 @@ function showScopeSavedSuccess(
134
151
  profileId: string,
135
152
  ): void {
136
153
  const scopeLabel = scope === "user" ? "user settings" : "project settings";
137
- api.ui.toast({
154
+ showToast(api, {
138
155
  variant: "success",
139
156
  title: "mem9 configured",
140
157
  message: `Saved ${scopeLabel} with profile ${profileId}. Restart OpenCode to reload mem9.`,
@@ -335,7 +352,7 @@ function showProfileIdDialog(
335
352
  onConfirm: (value) => {
336
353
  const next = value.trim();
337
354
  if (next.length === 0) {
338
- api.ui.toast({
355
+ showToast(api, {
339
356
  variant: "warning",
340
357
  message: "Profile ID is required.",
341
358
  });
@@ -343,7 +360,7 @@ function showProfileIdDialog(
343
360
  }
344
361
 
345
362
  if (isReusableProfileID(state, next)) {
346
- api.ui.toast({
363
+ showToast(api, {
347
364
  variant: "warning",
348
365
  message: "That profile already has credentials. Pick a new profile ID or use the existing profile in scope settings.",
349
366
  });
@@ -354,7 +371,9 @@ function showProfileIdDialog(
354
371
  if (draft.label.trim().length === 0) {
355
372
  draft.label = next === "default" ? "Personal" : next;
356
373
  }
357
- showProfileLabelDialog(api, paths, state, action, draft);
374
+ scheduleDialogTransition(() => {
375
+ showProfileLabelDialog(api, paths, state, action, draft);
376
+ });
358
377
  },
359
378
  onCancel: () => {
360
379
  showActionDialog(api, paths, state);
@@ -378,7 +397,7 @@ function showProfileLabelDialog(
378
397
  onConfirm: (value) => {
379
398
  const next = value.trim();
380
399
  if (next.length === 0) {
381
- api.ui.toast({
400
+ showToast(api, {
382
401
  variant: "warning",
383
402
  message: "Profile label is required.",
384
403
  });
@@ -386,10 +405,14 @@ function showProfileLabelDialog(
386
405
  }
387
406
 
388
407
  draft.label = next;
389
- showProfileBaseUrlDialog(api, paths, state, action, draft);
408
+ scheduleDialogTransition(() => {
409
+ showProfileBaseUrlDialog(api, paths, state, action, draft);
410
+ });
390
411
  },
391
412
  onCancel: () => {
392
- showProfileIdDialog(api, paths, state, action, draft);
413
+ scheduleDialogTransition(() => {
414
+ showProfileIdDialog(api, paths, state, action, draft);
415
+ });
393
416
  },
394
417
  }),
395
418
  );
@@ -410,7 +433,7 @@ function showProfileBaseUrlDialog(
410
433
  onConfirm: (value) => {
411
434
  const next = value.trim();
412
435
  if (next.length === 0) {
413
- api.ui.toast({
436
+ showToast(api, {
414
437
  variant: "warning",
415
438
  message: "mem9 API URL is required.",
416
439
  });
@@ -423,10 +446,14 @@ function showProfileBaseUrlDialog(
423
446
  return;
424
447
  }
425
448
 
426
- showProfileApiKeyDialog(api, paths, state, draft);
449
+ scheduleDialogTransition(() => {
450
+ showProfileApiKeyDialog(api, paths, state, draft);
451
+ });
427
452
  },
428
453
  onCancel: () => {
429
- showProfileLabelDialog(api, paths, state, action, draft);
454
+ scheduleDialogTransition(() => {
455
+ showProfileLabelDialog(api, paths, state, action, draft);
456
+ });
430
457
  },
431
458
  }),
432
459
  );
@@ -438,11 +465,14 @@ function showProfileApiKeyDialog(
438
465
  state: SetupState,
439
466
  draft: ProfileDraft,
440
467
  ): void {
441
- api.ui.toast({
442
- variant: "warning",
443
- message: "The current OpenCode prompt is plain text. Your API key stays visible while typing.",
444
- duration: 4000,
445
- });
468
+ if (!hasShownVisibleApiKeyWarning) {
469
+ hasShownVisibleApiKeyWarning = true;
470
+ showToast(api, {
471
+ variant: "warning",
472
+ message: "API key input is visible while typing.",
473
+ duration: 4000,
474
+ });
475
+ }
446
476
 
447
477
  api.ui.dialog.replace(() =>
448
478
  api.ui.DialogPrompt({
@@ -451,7 +481,7 @@ function showProfileApiKeyDialog(
451
481
  onConfirm: (value) => {
452
482
  const next = value.trim();
453
483
  if (next.length === 0) {
454
- api.ui.toast({
484
+ showToast(api, {
455
485
  variant: "warning",
456
486
  message: "mem9 API key is required.",
457
487
  });
@@ -462,7 +492,9 @@ function showProfileApiKeyDialog(
462
492
  void submitManualProfile(api, paths, draft);
463
493
  },
464
494
  onCancel: () => {
465
- showProfileBaseUrlDialog(api, paths, state, "manual-api-key", draft);
495
+ scheduleDialogTransition(() => {
496
+ showProfileBaseUrlDialog(api, paths, state, "manual-api-key", draft);
497
+ });
466
498
  },
467
499
  }),
468
500
  );
@@ -570,7 +602,7 @@ function showDefaultTimeoutDialog(
570
602
  onConfirm: (value) => {
571
603
  const parsed = parsePositiveInteger(value, "defaultTimeoutMs");
572
604
  if (parsed === null) {
573
- api.ui.toast({
605
+ showToast(api, {
574
606
  variant: "warning",
575
607
  message: "Default timeout must be a positive integer.",
576
608
  });
@@ -578,7 +610,9 @@ function showDefaultTimeoutDialog(
578
610
  }
579
611
 
580
612
  draft.defaultTimeoutMs = parsed;
581
- showSearchTimeoutDialog(api, paths, state, draft);
613
+ scheduleDialogTransition(() => {
614
+ showSearchTimeoutDialog(api, paths, state, draft);
615
+ });
582
616
  },
583
617
  onCancel: () => {
584
618
  showScopeDebugDialog(api, paths, state, draft);
@@ -601,7 +635,7 @@ function showSearchTimeoutDialog(
601
635
  onConfirm: (value) => {
602
636
  const parsed = parsePositiveInteger(value, "searchTimeoutMs");
603
637
  if (parsed === null) {
604
- api.ui.toast({
638
+ showToast(api, {
605
639
  variant: "warning",
606
640
  message: "Search timeout must be a positive integer.",
607
641
  });
@@ -612,7 +646,9 @@ function showSearchTimeoutDialog(
612
646
  void submitScopeConfigDraft(api, paths, draft);
613
647
  },
614
648
  onCancel: () => {
615
- showDefaultTimeoutDialog(api, paths, state, draft);
649
+ scheduleDialogTransition(() => {
650
+ showDefaultTimeoutDialog(api, paths, state, draft);
651
+ });
616
652
  },
617
653
  }),
618
654
  );