@adminide-stack/yantra-mobile 12.0.123 → 12.0.124-alpha.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.
@@ -1 +1 @@
1
- {"version":3,"file":"chatWidgets.js","sources":["../../../src/features/chat/chatWidgets.ts"],"sourcesContent":["/**\n * Widget envelopes in an assistant reply, for the mobile main chat.\n *\n * Skills end a reply with a card envelope in a fenced block:\n *\n * ```yantra:widget (or ```json)\n * {\"propsComponent\":\"travel\",\"propsConfiguration\":{\"contents\":{\"attachment\":{…}}}}\n * ```\n *\n * The web chat turns that into a card (parseChatWidget). The mobile chat had no\n * parser, so the envelope rendered as a raw JSON code block, and while\n * streaming it grew in place character by character. This pulls every envelope\n * out of the text and returns what the thread should show instead.\n */\n\nimport { CANVAS_DESIGN_WIDGET, canvasDesignFromWidget, parseTemplatesOpening, TEMPLATES_WIDGET } from 'common';\n\nexport interface ChatWidget {\n component: string;\n attachment: Record<string, unknown>;\n}\n\nexport interface ChatWidgetSplit {\n /** The reply with every widget envelope removed. */\n text: string;\n widgets: ChatWidget[];\n}\n\nconst WIDGET_FENCE = /```(?:yantra:widget|json)[^\\S\\n]*\\n?([\\s\\S]*?)```/gi;\n/** An envelope still streaming in: an opened widget fence with no closing fence yet. */\nconst OPEN_WIDGET_TAIL = /```(?:yantra:widget|json)[^\\S\\n]*\\n?\\s*\\{\\s*\"propsComponent\"[\\s\\S]*$/i;\n/** The same, before the JSON reaches the component key. */\nconst OPEN_YANTRA_WIDGET_TAIL = /```yantra:widget[\\s\\S]*$/i;\n\nfunction toWidget(body: string): ChatWidget | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body.trim());\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n const obj = parsed as {\n propsComponent?: unknown;\n propsConfiguration?: { contents?: { attachment?: unknown } };\n attachment?: unknown;\n };\n if (typeof obj.propsComponent !== 'string' || !obj.propsComponent) return null;\n // Templates to browse: every field optional, the opening sometimes flat in `contents`.\n if (obj.propsComponent === TEMPLATES_WIDGET) {\n const contents = obj.propsConfiguration?.contents as Record<string, unknown> | undefined;\n const opening = parseTemplatesOpening(contents?.attachment ?? obj.attachment ?? contents ?? {});\n return opening ? { component: TEMPLATES_WIDGET, attachment: { ...opening } } : null;\n }\n // A design made from a template, only with the canvas artifact it is.\n if (obj.propsComponent === CANVAS_DESIGN_WIDGET) {\n const design = canvasDesignFromWidget(obj as Record<string, unknown>);\n return design ? { component: CANVAS_DESIGN_WIDGET, attachment: { ...design } } : null;\n }\n // Canonical path, tolerating the flatter `attachment` the web parser also accepts.\n const attachment = obj.propsConfiguration?.contents?.attachment ?? obj.attachment;\n if (!attachment || typeof attachment !== 'object') return null;\n return { component: obj.propsComponent, attachment: attachment as Record<string, unknown> };\n}\n\nexport function splitChatWidgets(content: string, streaming = false): ChatWidgetSplit {\n const widgets: ChatWidget[] = [];\n let text = content.replace(WIDGET_FENCE, (match, body: string) => {\n const widget = toWidget(body);\n if (!widget) return match; // an ordinary ```json example stays in the answer\n widgets.push(widget);\n return '';\n });\n if (streaming) {\n // Hide an envelope that is still arriving rather than paint half of it.\n text = text.replace(OPEN_WIDGET_TAIL, '').replace(OPEN_YANTRA_WIDGET_TAIL, '');\n }\n return { text: text.replace(/\\n{3,}/g, '\\n\\n').trim(), widgets };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chatWidgets.js","sources":["../../../src/features/chat/chatWidgets.ts"],"sourcesContent":["/**\n * Widget envelopes in an assistant reply, for the mobile main chat.\n *\n * Skills end a reply with a card envelope in a fenced block:\n *\n * ```yantra:widget (or ```json)\n * {\"propsComponent\":\"travel\",\"propsConfiguration\":{\"contents\":{\"attachment\":{…}}}}\n * ```\n *\n * The web chat turns that into a card (parseChatWidget). The mobile chat had no\n * parser, so the envelope rendered as a raw JSON code block, and while\n * streaming it grew in place character by character. This pulls every envelope\n * out of the text and returns what the thread should show instead.\n *\n * A `templates` widget also takes over the Templates surface link the messenger\n * gateway ends the reply with when the agent asked for the surface\n * (`?ys=<snapshot>`, splitTemplatesSurfaceLink): its row opens that link, the\n * one that lets the Templates page send a pick back to this chat, and the line\n * leaves the text - one way into the same templates, not two.\n */\n\nimport {\n CANVAS_DESIGN_WIDGET,\n canvasDesignFromWidget,\n parseTemplatesOpening,\n splitTemplatesSurfaceLink,\n TEMPLATES_WIDGET,\n} from 'common';\n\nexport interface ChatWidget {\n component: string;\n attachment: Record<string, unknown>;\n /**\n * The surface link the reply carried for this widget (a `templates` widget's\n * Templates surface page, with the reply's snapshot). Absent on a reply that\n * had none - an older reply, or one where the agent asked for no surface.\n */\n link?: string;\n}\n\nexport interface ChatWidgetSplit {\n /** The reply with every widget envelope removed. */\n text: string;\n widgets: ChatWidget[];\n}\n\nconst WIDGET_FENCE = /```(?:yantra:widget|json)[^\\S\\n]*\\n?([\\s\\S]*?)```/gi;\n/** An envelope still streaming in: an opened widget fence with no closing fence yet. */\nconst OPEN_WIDGET_TAIL = /```(?:yantra:widget|json)[^\\S\\n]*\\n?\\s*\\{\\s*\"propsComponent\"[\\s\\S]*$/i;\n/** The same, before the JSON reaches the component key. */\nconst OPEN_YANTRA_WIDGET_TAIL = /```yantra:widget[\\s\\S]*$/i;\n\nfunction toWidget(body: string): ChatWidget | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body.trim());\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n const obj = parsed as {\n propsComponent?: unknown;\n propsConfiguration?: { contents?: { attachment?: unknown } };\n attachment?: unknown;\n };\n if (typeof obj.propsComponent !== 'string' || !obj.propsComponent) return null;\n // Templates to browse: every field optional, the opening sometimes flat in `contents`.\n if (obj.propsComponent === TEMPLATES_WIDGET) {\n const contents = obj.propsConfiguration?.contents as Record<string, unknown> | undefined;\n const opening = parseTemplatesOpening(contents?.attachment ?? obj.attachment ?? contents ?? {});\n return opening ? { component: TEMPLATES_WIDGET, attachment: { ...opening } } : null;\n }\n // A design made from a template, only with the canvas artifact it is.\n if (obj.propsComponent === CANVAS_DESIGN_WIDGET) {\n const design = canvasDesignFromWidget(obj as Record<string, unknown>);\n return design ? { component: CANVAS_DESIGN_WIDGET, attachment: { ...design } } : null;\n }\n // Canonical path, tolerating the flatter `attachment` the web parser also accepts.\n const attachment = obj.propsConfiguration?.contents?.attachment ?? obj.attachment;\n if (!attachment || typeof attachment !== 'object') return null;\n return { component: obj.propsComponent, attachment: attachment as Record<string, unknown> };\n}\n\nexport function splitChatWidgets(content: string, streaming = false): ChatWidgetSplit {\n const widgets: ChatWidget[] = [];\n let text = content.replace(WIDGET_FENCE, (match, body: string) => {\n const widget = toWidget(body);\n if (!widget) return match; // an ordinary ```json example stays in the answer\n widgets.push(widget);\n return '';\n });\n if (streaming) {\n // Hide an envelope that is still arriving rather than paint half of it.\n text = text.replace(OPEN_WIDGET_TAIL, '').replace(OPEN_YANTRA_WIDGET_TAIL, '');\n }\n // The gateway keeps the reply's LAST templates widget as the link's snapshot, so that one owns it.\n const templates = widgets.map((w) => w.component).lastIndexOf(TEMPLATES_WIDGET);\n if (templates >= 0) {\n const { text: rest, url } = splitTemplatesSurfaceLink(text);\n if (url) {\n text = rest;\n widgets[templates] = { ...widgets[templates], link: url };\n }\n }\n return { text: text.replace(/\\n{3,}/g, '\\n\\n').trim(), widgets };\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {titleFromPrompt,cleanPrompt,stripAgentNote}from'./buildFragments.js';var __defProp = Object.defineProperty;
1
+ import {titleFromPrompt,stripAgentNote,cleanPrompt}from'./buildFragments.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -32,7 +32,7 @@ function webBuilderPageUrl(webOrigin, channelId) {
32
32
  }
33
33
  function webBuilderJobRecord(start) {
34
34
  const brief = briefFrom(start.ask);
35
- const name = start.siteTitle || titleFromPrompt(brief) || "New site";
35
+ const name = start.siteTitle || titleFromPrompt(stripAgentNote(start.ask)) || "New site";
36
36
  const pageUrl = webBuilderPageUrl(start.webOrigin, start.channelId);
37
37
  return __spreadProps(__spreadValues({
38
38
  // One job per turn; the server keys it on `<surface>:<id>`, so a retry records nothing new.
@@ -1 +1 @@
1
- {"version":3,"file":"buildJob.js","sources":["../../../src/features/webbuilder/buildJob.ts"],"sourcesContent":["/**\n * A Web Builder turn (a new site, a remix or an edit) is a long-running job:\n * it is recorded as a server-owned surface job so it shows in the phone's Tasks\n * sheet and the web chat's Tasks panel while it runs, and after the app closes.\n *\n * Contract: adminIde project-mgmt `recordSurfaceJob(job)` / `updateSurfaceJob(id,\n * patch)` (surface-job-rules.ts); read back by common CENTRAL_TASKS_MODEL. The\n * agent settles a job whose ref rode on its chat turn (`job {task_id,\n * report_token}`); the app settles it too when it sees the turn end, and the\n * server's deadline sweep ends one nobody reported on.\n *\n * Pure (no React, no fetch): the host hook records and settles.\n */\nimport { cleanPrompt, stripAgentNote, titleFromPrompt } from './buildFragments';\nimport type { WebBuilderBuild } from './types';\n\n/** The slug every Web Builder job is recorded under. */\nexport const WEBBUILDER_JOB_SURFACE = 'webbuilder';\n/** A build or an edit finishes well inside this. */\nexport const WEBBUILDER_JOB_BUDGET_MS = 30 * 60 * 1000;\n/** Slack past the budget before a running job reads as stopped (surface-kit's grace). */\nexport const WEBBUILDER_JOB_GRACE_MS = 10 * 60 * 1000;\n/** The brief is a line under the title, not the whole ask. */\nconst BRIEF_MAX = 280;\n\n/** The ref a chat turn carries so the agent reports the job's verdict itself. */\nexport interface SurfaceJobRef {\n task_id: string;\n report_token: string;\n}\n\nexport type WebBuilderJobMode = 'new' | 'remix' | 'edit';\n\nexport interface WebBuilderJobStart {\n channelId: string;\n projectId?: string | null;\n /** The person's words for this turn, as sent (notes and prefixes are stripped). */\n ask: string;\n /** The site's name when it has one (an edit); else it is named from the ask. */\n siteTitle?: string | null;\n mode: WebBuilderJobMode;\n /** Who runs it: the agent chat, or the build workflow. */\n lane: 'agent' | 'workflow';\n startedAt: number;\n /** The web app's origin; the job opens this site's Web Builder there. */\n webOrigin?: string | null;\n}\n\nfunction briefFrom(ask: string): string {\n const text = cleanPrompt(stripAgentNote(ask));\n return text.length > BRIEF_MAX ? `${text.slice(0, BRIEF_MAX - 1).trimEnd()}…` : text;\n}\n\n/** `<origin>/c/<channelId>/webbuilder`, the web's Web Builder rail for this chat; http(s) only. */\nexport function webBuilderPageUrl(webOrigin: string | null | undefined, channelId: string): string | undefined {\n const origin = (webOrigin || '').trim().replace(/\\/+$/, '');\n if (!/^https?:\\/\\/[^/]+$/i.test(origin)) return undefined;\n return `${origin}/c/${encodeURIComponent(channelId)}/webbuilder`;\n}\n\n/** The `recordSurfaceJob` input for a turn that is starting. */\nexport function webBuilderJobRecord(start: WebBuilderJobStart): Record<string, unknown> {\n const brief = briefFrom(start.ask);\n const name = start.siteTitle || titleFromPrompt(brief) || 'New site';\n const pageUrl = webBuilderPageUrl(start.webOrigin, start.channelId);\n return {\n // One job per turn; the server keys it on `<surface>:<id>`, so a retry records nothing new.\n id: `${start.channelId}:${start.startedAt}`,\n surface: WEBBUILDER_JOB_SURFACE,\n lane: start.lane === 'agent' ? 'channel-turn' : 'workflow',\n title: start.mode === 'edit' ? `Update ${name}` : name,\n brief,\n state: 'running',\n createdAt: start.startedAt,\n startedAt: start.startedAt,\n deadlineAt: start.startedAt + WEBBUILDER_JOB_BUDGET_MS + WEBBUILDER_JOB_GRACE_MS,\n progress: start.mode === 'edit' ? 'Updating your app' : 'Starting your build',\n ...(pageUrl ? { pageUrl } : {}),\n handle: { channelId: start.channelId, ...(start.projectId ? { projectId: start.projectId } : {}) },\n };\n}\n\nexport type WebBuilderJobOutcome =\n | {\n ok: true;\n endedAt: number;\n build?: Pick<WebBuilderBuild, 'previewUrl' | 'liveUrl' | 'coverUrl' | 'title'> | null;\n }\n | { ok: false; endedAt: number; error: string };\n\n/** The `updateSurfaceJob` patch that ends a turn's job. */\nexport function webBuilderJobFinish(outcome: WebBuilderJobOutcome): Record<string, unknown> {\n if (!outcome.ok) {\n return { state: 'failed', endedAt: outcome.endedAt, progress: null, error: outcome.error };\n }\n const cover = outcome.build?.coverUrl;\n const results =\n cover && /^https?:\\/\\//i.test(cover)\n ? [{ url: cover, kind: 'image', title: outcome.build?.title || 'Site' }]\n : [];\n const site = outcome.build?.liveUrl || outcome.build?.previewUrl;\n return {\n state: 'succeeded',\n endedAt: outcome.endedAt,\n progress: null,\n ...(results.length ? { results } : {}),\n ...(site && /^https?:\\/\\//i.test(site) ? { resultUrl: site } : {}),\n };\n}\n\n/** The job's ref for a chat turn, when the server gave it a report token. */\nexport function surfaceJobRef(\n record: { id?: string | null; reportToken?: string | null } | null | undefined,\n): SurfaceJobRef | null {\n if (!record?.id || !record.reportToken) return null;\n return { task_id: record.id, report_token: record.reportToken };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"buildJob.js","sources":["../../../src/features/webbuilder/buildJob.ts"],"sourcesContent":["/**\n * A Web Builder turn (a new site, a remix or an edit) is a long-running job:\n * it is recorded as a server-owned surface job so it shows in the phone's Tasks\n * sheet and the web chat's Tasks panel while it runs, and after the app closes.\n *\n * Contract: adminIde project-mgmt `recordSurfaceJob(job)` / `updateSurfaceJob(id,\n * patch)` (surface-job-rules.ts); read back by common CENTRAL_TASKS_MODEL. The\n * agent settles a job whose ref rode on its chat turn (`job {task_id,\n * report_token}`); the app settles it too when it sees the turn end, and the\n * server's deadline sweep ends one nobody reported on.\n *\n * Pure (no React, no fetch): the host hook records and settles.\n */\nimport { cleanPrompt, stripAgentNote, titleFromPrompt } from './buildFragments';\nimport type { WebBuilderBuild } from './types';\n\n/** The slug every Web Builder job is recorded under. */\nexport const WEBBUILDER_JOB_SURFACE = 'webbuilder';\n/** A build or an edit finishes well inside this. */\nexport const WEBBUILDER_JOB_BUDGET_MS = 30 * 60 * 1000;\n/** Slack past the budget before a running job reads as stopped (surface-kit's grace). */\nexport const WEBBUILDER_JOB_GRACE_MS = 10 * 60 * 1000;\n/** The brief is a line under the title, not the whole ask. */\nconst BRIEF_MAX = 280;\n\n/** The ref a chat turn carries so the agent reports the job's verdict itself. */\nexport interface SurfaceJobRef {\n task_id: string;\n report_token: string;\n}\n\nexport type WebBuilderJobMode = 'new' | 'remix' | 'edit';\n\nexport interface WebBuilderJobStart {\n channelId: string;\n projectId?: string | null;\n /** The person's words for this turn, as sent (notes and prefixes are stripped). */\n ask: string;\n /** The site's name when it has one (an edit); else it is named from the ask. */\n siteTitle?: string | null;\n mode: WebBuilderJobMode;\n /** Who runs it: the agent chat, or the build workflow. */\n lane: 'agent' | 'workflow';\n startedAt: number;\n /** The web app's origin; the job opens this site's Web Builder there. */\n webOrigin?: string | null;\n}\n\nfunction briefFrom(ask: string): string {\n const text = cleanPrompt(stripAgentNote(ask));\n return text.length > BRIEF_MAX ? `${text.slice(0, BRIEF_MAX - 1).trimEnd()}…` : text;\n}\n\n/** `<origin>/c/<channelId>/webbuilder`, the web's Web Builder rail for this chat; http(s) only. */\nexport function webBuilderPageUrl(webOrigin: string | null | undefined, channelId: string): string | undefined {\n const origin = (webOrigin || '').trim().replace(/\\/+$/, '');\n if (!/^https?:\\/\\/[^/]+$/i.test(origin)) return undefined;\n return `${origin}/c/${encodeURIComponent(channelId)}/webbuilder`;\n}\n\n/** The `recordSurfaceJob` input for a turn that is starting. */\nexport function webBuilderJobRecord(start: WebBuilderJobStart): Record<string, unknown> {\n const brief = briefFrom(start.ask);\n // Named from the ask as sent: its \"Build me:\" marks the brief, so \"Build me: Make me a pin\n // board\" is \"Pin Board\" - without it \"Make me ...\" reads as an edit and gets no name.\n const name = start.siteTitle || titleFromPrompt(stripAgentNote(start.ask)) || 'New site';\n const pageUrl = webBuilderPageUrl(start.webOrigin, start.channelId);\n return {\n // One job per turn; the server keys it on `<surface>:<id>`, so a retry records nothing new.\n id: `${start.channelId}:${start.startedAt}`,\n surface: WEBBUILDER_JOB_SURFACE,\n lane: start.lane === 'agent' ? 'channel-turn' : 'workflow',\n title: start.mode === 'edit' ? `Update ${name}` : name,\n brief,\n state: 'running',\n createdAt: start.startedAt,\n startedAt: start.startedAt,\n deadlineAt: start.startedAt + WEBBUILDER_JOB_BUDGET_MS + WEBBUILDER_JOB_GRACE_MS,\n progress: start.mode === 'edit' ? 'Updating your app' : 'Starting your build',\n ...(pageUrl ? { pageUrl } : {}),\n handle: { channelId: start.channelId, ...(start.projectId ? { projectId: start.projectId } : {}) },\n };\n}\n\nexport type WebBuilderJobOutcome =\n | {\n ok: true;\n endedAt: number;\n build?: Pick<WebBuilderBuild, 'previewUrl' | 'liveUrl' | 'coverUrl' | 'title'> | null;\n }\n | { ok: false; endedAt: number; error: string };\n\n/** The `updateSurfaceJob` patch that ends a turn's job. */\nexport function webBuilderJobFinish(outcome: WebBuilderJobOutcome): Record<string, unknown> {\n if (!outcome.ok) {\n return { state: 'failed', endedAt: outcome.endedAt, progress: null, error: outcome.error };\n }\n const cover = outcome.build?.coverUrl;\n const results =\n cover && /^https?:\\/\\//i.test(cover)\n ? [{ url: cover, kind: 'image', title: outcome.build?.title || 'Site' }]\n : [];\n const site = outcome.build?.liveUrl || outcome.build?.previewUrl;\n return {\n state: 'succeeded',\n endedAt: outcome.endedAt,\n progress: null,\n ...(results.length ? { results } : {}),\n ...(site && /^https?:\\/\\//i.test(site) ? { resultUrl: site } : {}),\n };\n}\n\n/** The job's ref for a chat turn, when the server gave it a report token. */\nexport function surfaceJobRef(\n record: { id?: string | null; reportToken?: string | null } | null | undefined,\n): SurfaceJobRef | null {\n if (!record?.id || !record.reportToken) return null;\n return { task_id: record.id, report_token: record.reportToken };\n}\n"],"names":[],"mappings":"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"}
@@ -25,6 +25,7 @@ function useCdecliAutoConnect(isSelected, channelId) {
25
25
  const [activeSkill, setActiveSkill] = useState(null);
26
26
  const connectingRef = useRef(false);
27
27
  const connectedRef = useRef(false);
28
+ const attemptRef = useRef(0);
28
29
  const {
29
30
  accountUserId,
30
31
  orgName,
@@ -119,6 +120,7 @@ function useCdecliAutoConnect(isSelected, channelId) {
119
120
  }
120
121
  }
121
122
  });
123
+ if (opts.attempt !== attemptRef.current) return true;
122
124
  return applyConnectResult(data);
123
125
  }, [connectMutation, accountId, endpoint, channelId, applyConnectResult]);
124
126
  useEffect(() => {
@@ -140,11 +142,15 @@ function useCdecliAutoConnect(isSelected, channelId) {
140
142
  }
141
143
  const doConnect = async () => {
142
144
  var _a;
145
+ attemptRef.current += 1;
146
+ const attempt = attemptRef.current;
147
+ const superseded = () => attempt !== attemptRef.current;
143
148
  connectingRef.current = true;
144
149
  if (!isSilentReconnect) setStatus("connecting");
145
150
  setError(null);
146
151
  try {
147
152
  const token = await obtainTokenForAgent(false);
153
+ if (superseded()) return;
148
154
  if (!token) {
149
155
  if (mintOnCluster) {
150
156
  setStatus("error");
@@ -171,16 +177,21 @@ function useCdecliAutoConnect(isSelected, channelId) {
171
177
  }
172
178
  }
173
179
  });
180
+ if (superseded()) return;
174
181
  console.log("[useCdecliAutoConnect] connectMutation result (account=%s mintOnCluster=%s):", accountId, mintOnCluster, JSON.stringify(data));
175
182
  applyConnectResult(data);
176
183
  } catch (err) {
177
184
  const msg = err instanceof Error ? err.message : String(err);
185
+ if (superseded()) {
186
+ console.warn("[useCdecliAutoConnect] superseded connect failed (ignored):", msg);
187
+ return;
188
+ }
178
189
  console.error("[useCdecliAutoConnect] connect error:", msg);
179
190
  setStatus("error");
180
191
  setError(msg);
181
192
  connectedRef.current = false;
182
193
  } finally {
183
- connectingRef.current = false;
194
+ if (!superseded()) connectingRef.current = false;
184
195
  }
185
196
  };
186
197
  doConnect();
@@ -188,11 +199,14 @@ function useCdecliAutoConnect(isSelected, channelId) {
188
199
  const connectWithSkill = useCallback(async (skill, model, systemPrompt, skillId) => {
189
200
  var _a;
190
201
  console.log("[useCdecliAutoConnect] connectWithSkill called with skill=%s skillId=%s model=%s systemPrompt=%d chars", skill, skillId != null ? skillId : "", model, (_a = systemPrompt == null ? void 0 : systemPrompt.length) != null ? _a : 0);
202
+ attemptRef.current += 1;
203
+ const attempt = attemptRef.current;
191
204
  setStatus("connecting");
192
205
  setError(null);
193
206
  try {
194
207
  const token = await obtainTokenForAgent(false);
195
208
  await runConnect({
209
+ attempt,
196
210
  token,
197
211
  skill,
198
212
  skillId,
@@ -203,14 +217,18 @@ function useCdecliAutoConnect(isSelected, channelId) {
203
217
  } catch (err) {
204
218
  const msg = err instanceof Error ? err.message : String(err);
205
219
  console.error("[useCdecliAutoConnect] connectWithSkill error:", msg);
220
+ if (attempt !== attemptRef.current) return;
206
221
  setStatus("error");
207
222
  setError(msg);
208
223
  }
209
224
  }, [obtainTokenForAgent, runConnect]);
210
225
  const recoverAgentAuth = useCallback(async () => {
211
226
  var _a;
227
+ attemptRef.current += 1;
228
+ const attempt = attemptRef.current;
212
229
  try {
213
230
  const token = await obtainTokenForAgent(true);
231
+ if (attempt !== attemptRef.current) return true;
214
232
  if (!token) {
215
233
  setStatus("error");
216
234
  setError((_a = mintErrorRef.current) != null ? _a : "Could not remint the CDeCLI system token. Sign in again, then retry.");
@@ -218,13 +236,15 @@ function useCdecliAutoConnect(isSelected, channelId) {
218
236
  return false;
219
237
  }
220
238
  const ok = await runConnect({
239
+ attempt,
221
240
  token
222
241
  });
223
- if (ok) setError(null);
242
+ if (ok && attempt === attemptRef.current) setError(null);
224
243
  return ok;
225
244
  } catch (err) {
226
245
  const msg = err instanceof Error ? err.message : String(err);
227
246
  console.error("[useCdecliAutoConnect] recoverAgentAuth error:", msg);
247
+ if (attempt !== attemptRef.current) return false;
228
248
  setStatus("error");
229
249
  setError(msg);
230
250
  connectedRef.current = false;
@@ -1 +1 @@
1
- {"version":3,"file":"useCdecliAutoConnect.js","sources":["../../src/hooks/useCdecliAutoConnect.ts"],"sourcesContent":["/**\n * Auto-connects the cdecli-serve messenger-gateway channel when the CDeCLI\n * gateway is selected from the dropdown. Obtains/creates a system token and\n * calls the `gatewayConnect` mutation so the backend provider has a live\n * session with the cdecli-agent endpoint.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useGatewayConnectMutation } from 'common/graphql';\nimport { config } from '../config/env-config';\nimport { usePrerequisiteIds } from './usePrerequisiteIds';\nimport { mintSurfaceSystemToken } from './mintSurfaceSystemToken';\nimport { useObtainSystemToken } from './useObtainSystemToken';\nimport { resolveCdecliAgentTokenGraphqlUrl } from './systemTokenJwt';\nimport { isWorkspaceBootstrapPending } from './workspaceBootstrapPending';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type CdecliAutoConnectStatus = 'idle' | 'connecting' | 'connected' | 'error';\n\nexport interface UseCdecliAutoConnectResult {\n /** Whether the backend cdecli-serve channel is connected. */\n channelConnected: boolean;\n /** Current connection status. */\n status: CdecliAutoConnectStatus;\n /** Error message if status is 'error'. */\n error: string | null;\n /** Persistence mode reported by the backend provider ('backend' | 'frontend'). */\n persistenceMode: 'backend' | 'frontend' | undefined;\n /** Active skill name on the current CDeCLI session (null if none). */\n activeSkill: string | null;\n /** Per-user accountId used for this gateway connection. */\n accountId: string;\n /** Reconnect the CDeCLI session with a specific skill pre-activated. */\n connectWithSkill: (skill: string, model?: string, systemPrompt?: string, skillId?: string) => Promise<void>;\n /**\n * Revoke the dead system JWT, remint against the agent-trusted backend, and\n * reconnect. Used when cdecli-agent 401s session create (invalid HS256 signature).\n */\n recoverAgentAuth: () => Promise<boolean>;\n /**\n * The token this connection hands the agent: cdecli names the caller by its subject, and the person's Computer\n * is resolved from that. A call the app makes on that Computer itself (the one-time code card typing into the\n * page) carries the same token, so it reaches the Computer the agent is driving - another token's subject is a\n * different Computer, or none. Null when it cannot be had (not signed in yet, mint failed).\n */\n obtainAgentToken: () => Promise<string | null>;\n /** The cdecli-agent this connection talks to (CDECLI_AGENT_ENDPOINT). */\n agentEndpoint: string;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nconst CHANNEL_TYPE = 'cdecli-serve';\n\nfunction normalizeGraphqlUrl(url: string): string {\n return url.trim().replace(/\\/$/, '');\n}\n\n// ─── Hook ────────────────────────────────────────────────────────\n\n/**\n * When `isSelected` is true, auto-connects the cdecli-serve channel on the\n * messenger-gateway backend using the hosted endpoint and a system token.\n */\nexport function useCdecliAutoConnect(isSelected: boolean, channelId?: string): UseCdecliAutoConnectResult {\n const [status, setStatus] = useState<CdecliAutoConnectStatus>('idle');\n const [error, setError] = useState<string | null>(null);\n const [persistenceMode, setPersistenceMode] = useState<'backend' | 'frontend' | undefined>(undefined);\n const [activeSkill, setActiveSkill] = useState<string | null>(null);\n const connectingRef = useRef(false);\n const connectedRef = useRef(false);\n\n const { accountUserId, orgName, projectId, tagId, loading: prerequisitesLoading } = usePrerequisiteIds();\n\n // Per-user accountId so each user gets their own cdecli-agent session.\n // Wait for the profile query to load before connecting — otherwise all\n // users fall back to 'default' and share a single cdecli-agent session.\n const accountId = accountUserId ?? 'default';\n\n const prevChannelIdRef = useRef<string | undefined>(channelId);\n /** Why the last cluster mint returned null, so callers can show the real reason. */\n const mintErrorRef = useRef<string | null>(null);\n\n const [connectMutation] = useGatewayConnectMutation();\n\n const endpoint = (config.CDECLI_AGENT_ENDPOINT || 'https://cdecli-agent.cdebase.dev').trim();\n const tokenGraphqlUrl = useMemo(\n () => resolveCdecliAgentTokenGraphqlUrl(config.GRAPHQL_URL, endpoint, config.CANVAS_BACKEND_GRAPHQL_URL),\n [endpoint],\n );\n const mintOnCluster = normalizeGraphqlUrl(tokenGraphqlUrl) !== normalizeGraphqlUrl(config.GRAPHQL_URL);\n\n const {\n loading: localTokenLoading,\n obtainSystemToken,\n regenerateSystemToken,\n } = useObtainSystemToken(isSelected && !mintOnCluster);\n\n const obtainTokenForAgent = useCallback(\n async (discardExistingRow = false): Promise<string | null> => {\n if (mintOnCluster) {\n // Same gate web applies before minting (browser useCdecliAutoConnect):\n // no org/project yet means the workspace bootstrap is still running and\n // scope discovery would fail. Skip quietly - the effect re-runs once\n // usePrerequisiteIds resolves them.\n if (!orgName || !projectId) {\n mintErrorRef.current = 'Your workspace is still being set up - this will connect once it finishes.';\n return null;\n }\n try {\n const minted = await mintSurfaceSystemToken({\n graphqlUrl: tokenGraphqlUrl,\n // Preferred scope: kept whenever the cluster knows it (see mintScope.ts).\n orgName,\n projectId,\n tagId,\n discoverScope: true,\n forceRemint: discardExistingRow,\n });\n mintErrorRef.current = null;\n return minted.token?.trim() || null;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const pending = isWorkspaceBootstrapPending(msg);\n // Keep the specific reason. The generic caller message used to hide\n // genuinely actionable text like \"No organization on the canvas\n // backend. Sign in once in the canvas harness, then retry.\"\n mintErrorRef.current = pending ? `${msg} (minting against ${tokenGraphqlUrl})` : msg;\n if (pending) {\n console.warn('[useCdecliAutoConnect] cluster system-token mint not ready yet:', msg);\n } else {\n console.error('[useCdecliAutoConnect] cluster system-token mint failed:', err);\n }\n return null;\n }\n }\n return discardExistingRow ? regenerateSystemToken() : obtainSystemToken();\n },\n [mintOnCluster, tokenGraphqlUrl, orgName, projectId, tagId, obtainSystemToken, regenerateSystemToken],\n );\n\n const applyConnectResult = useCallback((data: { gatewayConnect?: unknown } | null | undefined) => {\n const result = data?.gatewayConnect as\n | {\n persistenceMode?: string | null;\n state?: string | null;\n connected?: boolean | null;\n lastError?: string | null;\n }\n | undefined;\n if (result?.persistenceMode) {\n setPersistenceMode(result.persistenceMode as 'backend' | 'frontend');\n }\n if (result?.state === 'ERROR') {\n setStatus('error');\n setError(result.lastError ?? 'Failed to connect to CDeCLI agent');\n connectedRef.current = false;\n return false;\n }\n setStatus('connected');\n connectedRef.current = true;\n return true;\n }, []);\n\n const runConnect = useCallback(\n async (opts: {\n token: string | null;\n skill?: string;\n skillId?: string;\n model?: string;\n systemPrompt?: string;\n }) => {\n const { data } = await connectMutation({\n variables: {\n input: {\n channelType: CHANNEL_TYPE,\n accountId,\n options: {\n endpoint,\n ...(opts.token ? { token: opts.token } : {}),\n ...(channelId ? { chatId: channelId } : {}),\n ...(opts.skill ? { skill: opts.skill } : {}),\n ...(opts.skillId ? { skillId: opts.skillId } : {}),\n ...(opts.model ? { model: opts.model } : {}),\n ...(opts.systemPrompt ? { systemPrompt: opts.systemPrompt } : {}),\n },\n },\n },\n });\n return applyConnectResult(data);\n },\n [connectMutation, accountId, endpoint, channelId, applyConnectResult],\n );\n\n // Auto-connect when CDeCLI gateway is selected\n useEffect(() => {\n if (!isSelected) {\n connectingRef.current = false;\n return;\n }\n\n if (prerequisitesLoading || !accountUserId) return;\n\n if (!mintOnCluster && localTokenLoading) return;\n\n let isSilentReconnect = false;\n if (connectedRef.current || connectingRef.current) {\n if (channelId && prevChannelIdRef.current !== channelId) {\n isSilentReconnect = connectedRef.current;\n prevChannelIdRef.current = channelId;\n connectedRef.current = false;\n } else {\n return;\n }\n }\n\n const doConnect = async () => {\n connectingRef.current = true;\n if (!isSilentReconnect) setStatus('connecting');\n setError(null);\n\n try {\n const token = await obtainTokenForAgent(false);\n if (!token) {\n if (mintOnCluster) {\n setStatus('error');\n setError(mintErrorRef.current ?? 'Could not mint a system token for the hosted CDeCLI agent.');\n connectedRef.current = false;\n return;\n }\n console.warn('[useCdecliAutoConnect] No system token, connecting without auth');\n }\n\n const { data } = await connectMutation({\n variables: {\n input: {\n channelType: CHANNEL_TYPE,\n accountId,\n options: {\n endpoint,\n ...(token ? { token } : {}),\n ...(channelId ? { chatId: channelId } : {}),\n },\n },\n },\n });\n\n console.log(\n '[useCdecliAutoConnect] connectMutation result (account=%s mintOnCluster=%s):',\n accountId,\n mintOnCluster,\n JSON.stringify(data),\n );\n applyConnectResult(data);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error('[useCdecliAutoConnect] connect error:', msg);\n setStatus('error');\n setError(msg);\n connectedRef.current = false;\n } finally {\n connectingRef.current = false;\n }\n };\n\n doConnect();\n }, [\n isSelected,\n obtainTokenForAgent,\n connectMutation,\n applyConnectResult,\n accountId,\n prerequisitesLoading,\n accountUserId,\n channelId,\n localTokenLoading,\n mintOnCluster,\n endpoint,\n ]);\n\n const connectWithSkill = useCallback(\n async (skill: string, model?: string, systemPrompt?: string, skillId?: string) => {\n console.log(\n '[useCdecliAutoConnect] connectWithSkill called with skill=%s skillId=%s model=%s systemPrompt=%d chars',\n skill,\n skillId ?? '',\n model,\n systemPrompt?.length ?? 0,\n );\n setStatus('connecting');\n setError(null);\n\n try {\n const token = await obtainTokenForAgent(false);\n await runConnect({ token, skill, skillId, model, systemPrompt });\n setActiveSkill(skill);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error('[useCdecliAutoConnect] connectWithSkill error:', msg);\n setStatus('error');\n setError(msg);\n }\n },\n [obtainTokenForAgent, runConnect],\n );\n\n const recoverAgentAuth = useCallback(async (): Promise<boolean> => {\n try {\n const token = await obtainTokenForAgent(true);\n if (!token) {\n setStatus('error');\n setError(\n mintErrorRef.current ?? 'Could not remint the CDeCLI system token. Sign in again, then retry.',\n );\n connectedRef.current = false;\n return false;\n }\n const ok = await runConnect({ token });\n if (ok) setError(null);\n return ok;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error('[useCdecliAutoConnect] recoverAgentAuth error:', msg);\n setStatus('error');\n setError(msg);\n connectedRef.current = false;\n return false;\n }\n }, [obtainTokenForAgent, runConnect]);\n\n // The same token the connect above hands the agent (a reveal of the one system token row, or the cluster mint),\n // never a fresh one: rotating it here would leave the chat's session on a revoked token.\n const obtainAgentToken = useCallback(() => obtainTokenForAgent(false), [obtainTokenForAgent]);\n\n return {\n channelConnected: status === 'connected',\n status,\n error,\n persistenceMode,\n activeSkill,\n accountId,\n connectWithSkill,\n recoverAgentAuth,\n obtainAgentToken,\n agentEndpoint: endpoint,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useCdecliAutoConnect.js","sources":["../../src/hooks/useCdecliAutoConnect.ts"],"sourcesContent":["/**\n * Auto-connects the cdecli-serve messenger-gateway channel when the CDeCLI\n * gateway is selected from the dropdown. Obtains/creates a system token and\n * calls the `gatewayConnect` mutation so the backend provider has a live\n * session with the cdecli-agent endpoint.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useGatewayConnectMutation } from 'common/graphql';\nimport { config } from '../config/env-config';\nimport { usePrerequisiteIds } from './usePrerequisiteIds';\nimport { mintSurfaceSystemToken } from './mintSurfaceSystemToken';\nimport { useObtainSystemToken } from './useObtainSystemToken';\nimport { resolveCdecliAgentTokenGraphqlUrl } from './systemTokenJwt';\nimport { isWorkspaceBootstrapPending } from './workspaceBootstrapPending';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport type CdecliAutoConnectStatus = 'idle' | 'connecting' | 'connected' | 'error';\n\nexport interface UseCdecliAutoConnectResult {\n /** Whether the backend cdecli-serve channel is connected. */\n channelConnected: boolean;\n /** Current connection status. */\n status: CdecliAutoConnectStatus;\n /** Error message if status is 'error'. */\n error: string | null;\n /** Persistence mode reported by the backend provider ('backend' | 'frontend'). */\n persistenceMode: 'backend' | 'frontend' | undefined;\n /** Active skill name on the current CDeCLI session (null if none). */\n activeSkill: string | null;\n /** Per-user accountId used for this gateway connection. */\n accountId: string;\n /** Reconnect the CDeCLI session with a specific skill pre-activated. */\n connectWithSkill: (skill: string, model?: string, systemPrompt?: string, skillId?: string) => Promise<void>;\n /**\n * Revoke the dead system JWT, remint against the agent-trusted backend, and\n * reconnect. Used when cdecli-agent 401s session create (invalid HS256 signature).\n */\n recoverAgentAuth: () => Promise<boolean>;\n /**\n * The token this connection hands the agent: cdecli names the caller by its subject, and the person's Computer\n * is resolved from that. A call the app makes on that Computer itself (the one-time code card typing into the\n * page) carries the same token, so it reaches the Computer the agent is driving - another token's subject is a\n * different Computer, or none. Null when it cannot be had (not signed in yet, mint failed).\n */\n obtainAgentToken: () => Promise<string | null>;\n /** The cdecli-agent this connection talks to (CDECLI_AGENT_ENDPOINT). */\n agentEndpoint: string;\n}\n\n// ─── Constants ───────────────────────────────────────────────────\n\nconst CHANNEL_TYPE = 'cdecli-serve';\n\nfunction normalizeGraphqlUrl(url: string): string {\n return url.trim().replace(/\\/$/, '');\n}\n\n// ─── Hook ────────────────────────────────────────────────────────\n\n/**\n * When `isSelected` is true, auto-connects the cdecli-serve channel on the\n * messenger-gateway backend using the hosted endpoint and a system token.\n */\nexport function useCdecliAutoConnect(isSelected: boolean, channelId?: string): UseCdecliAutoConnectResult {\n const [status, setStatus] = useState<CdecliAutoConnectStatus>('idle');\n const [error, setError] = useState<string | null>(null);\n const [persistenceMode, setPersistenceMode] = useState<'backend' | 'frontend' | undefined>(undefined);\n const [activeSkill, setActiveSkill] = useState<string | null>(null);\n const connectingRef = useRef(false);\n const connectedRef = useRef(false);\n /**\n * Each connect takes a number; only the latest may write the status. A chat switch\n * starts a new connect while the last one may still be minting its token, and that\n * superseded attempt failing later must not mark the live connection as down (the\n * composer then reads \"Connecting…\" and refuses to send while the agent works).\n */\n const attemptRef = useRef(0);\n\n const { accountUserId, orgName, projectId, tagId, loading: prerequisitesLoading } = usePrerequisiteIds();\n\n // Per-user accountId so each user gets their own cdecli-agent session.\n // Wait for the profile query to load before connecting — otherwise all\n // users fall back to 'default' and share a single cdecli-agent session.\n const accountId = accountUserId ?? 'default';\n\n const prevChannelIdRef = useRef<string | undefined>(channelId);\n /** Why the last cluster mint returned null, so callers can show the real reason. */\n const mintErrorRef = useRef<string | null>(null);\n\n const [connectMutation] = useGatewayConnectMutation();\n\n const endpoint = (config.CDECLI_AGENT_ENDPOINT || 'https://cdecli-agent.cdebase.dev').trim();\n const tokenGraphqlUrl = useMemo(\n () => resolveCdecliAgentTokenGraphqlUrl(config.GRAPHQL_URL, endpoint, config.CANVAS_BACKEND_GRAPHQL_URL),\n [endpoint],\n );\n const mintOnCluster = normalizeGraphqlUrl(tokenGraphqlUrl) !== normalizeGraphqlUrl(config.GRAPHQL_URL);\n\n const {\n loading: localTokenLoading,\n obtainSystemToken,\n regenerateSystemToken,\n } = useObtainSystemToken(isSelected && !mintOnCluster);\n\n const obtainTokenForAgent = useCallback(\n async (discardExistingRow = false): Promise<string | null> => {\n if (mintOnCluster) {\n // Same gate web applies before minting (browser useCdecliAutoConnect):\n // no org/project yet means the workspace bootstrap is still running and\n // scope discovery would fail. Skip quietly - the effect re-runs once\n // usePrerequisiteIds resolves them.\n if (!orgName || !projectId) {\n mintErrorRef.current = 'Your workspace is still being set up - this will connect once it finishes.';\n return null;\n }\n try {\n const minted = await mintSurfaceSystemToken({\n graphqlUrl: tokenGraphqlUrl,\n // Preferred scope: kept whenever the cluster knows it (see mintScope.ts).\n orgName,\n projectId,\n tagId,\n discoverScope: true,\n forceRemint: discardExistingRow,\n });\n mintErrorRef.current = null;\n return minted.token?.trim() || null;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const pending = isWorkspaceBootstrapPending(msg);\n // Keep the specific reason. The generic caller message used to hide\n // genuinely actionable text like \"No organization on the canvas\n // backend. Sign in once in the canvas harness, then retry.\"\n mintErrorRef.current = pending ? `${msg} (minting against ${tokenGraphqlUrl})` : msg;\n if (pending) {\n console.warn('[useCdecliAutoConnect] cluster system-token mint not ready yet:', msg);\n } else {\n console.error('[useCdecliAutoConnect] cluster system-token mint failed:', err);\n }\n return null;\n }\n }\n return discardExistingRow ? regenerateSystemToken() : obtainSystemToken();\n },\n [mintOnCluster, tokenGraphqlUrl, orgName, projectId, tagId, obtainSystemToken, regenerateSystemToken],\n );\n\n const applyConnectResult = useCallback((data: { gatewayConnect?: unknown } | null | undefined) => {\n const result = data?.gatewayConnect as\n | {\n persistenceMode?: string | null;\n state?: string | null;\n connected?: boolean | null;\n lastError?: string | null;\n }\n | undefined;\n if (result?.persistenceMode) {\n setPersistenceMode(result.persistenceMode as 'backend' | 'frontend');\n }\n if (result?.state === 'ERROR') {\n setStatus('error');\n setError(result.lastError ?? 'Failed to connect to CDeCLI agent');\n connectedRef.current = false;\n return false;\n }\n setStatus('connected');\n connectedRef.current = true;\n return true;\n }, []);\n\n const runConnect = useCallback(\n async (opts: {\n attempt: number;\n token: string | null;\n skill?: string;\n skillId?: string;\n model?: string;\n systemPrompt?: string;\n }) => {\n const { data } = await connectMutation({\n variables: {\n input: {\n channelType: CHANNEL_TYPE,\n accountId,\n options: {\n endpoint,\n ...(opts.token ? { token: opts.token } : {}),\n ...(channelId ? { chatId: channelId } : {}),\n ...(opts.skill ? { skill: opts.skill } : {}),\n ...(opts.skillId ? { skillId: opts.skillId } : {}),\n ...(opts.model ? { model: opts.model } : {}),\n ...(opts.systemPrompt ? { systemPrompt: opts.systemPrompt } : {}),\n },\n },\n },\n });\n // A newer connect owns the status now; this result is not the current one.\n if (opts.attempt !== attemptRef.current) return true;\n return applyConnectResult(data);\n },\n [connectMutation, accountId, endpoint, channelId, applyConnectResult],\n );\n\n // Auto-connect when CDeCLI gateway is selected\n useEffect(() => {\n if (!isSelected) {\n connectingRef.current = false;\n return;\n }\n\n if (prerequisitesLoading || !accountUserId) return;\n\n if (!mintOnCluster && localTokenLoading) return;\n\n let isSilentReconnect = false;\n if (connectedRef.current || connectingRef.current) {\n if (channelId && prevChannelIdRef.current !== channelId) {\n isSilentReconnect = connectedRef.current;\n prevChannelIdRef.current = channelId;\n connectedRef.current = false;\n } else {\n return;\n }\n }\n\n const doConnect = async () => {\n attemptRef.current += 1;\n const attempt = attemptRef.current;\n const superseded = () => attempt !== attemptRef.current;\n connectingRef.current = true;\n if (!isSilentReconnect) setStatus('connecting');\n setError(null);\n\n try {\n const token = await obtainTokenForAgent(false);\n if (superseded()) return;\n if (!token) {\n if (mintOnCluster) {\n setStatus('error');\n setError(mintErrorRef.current ?? 'Could not mint a system token for the hosted CDeCLI agent.');\n connectedRef.current = false;\n return;\n }\n console.warn('[useCdecliAutoConnect] No system token, connecting without auth');\n }\n\n const { data } = await connectMutation({\n variables: {\n input: {\n channelType: CHANNEL_TYPE,\n accountId,\n options: {\n endpoint,\n ...(token ? { token } : {}),\n ...(channelId ? { chatId: channelId } : {}),\n },\n },\n },\n });\n\n if (superseded()) return;\n console.log(\n '[useCdecliAutoConnect] connectMutation result (account=%s mintOnCluster=%s):',\n accountId,\n mintOnCluster,\n JSON.stringify(data),\n );\n applyConnectResult(data);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (superseded()) {\n console.warn('[useCdecliAutoConnect] superseded connect failed (ignored):', msg);\n return;\n }\n console.error('[useCdecliAutoConnect] connect error:', msg);\n setStatus('error');\n setError(msg);\n connectedRef.current = false;\n } finally {\n if (!superseded()) connectingRef.current = false;\n }\n };\n\n doConnect();\n }, [\n isSelected,\n obtainTokenForAgent,\n connectMutation,\n applyConnectResult,\n accountId,\n prerequisitesLoading,\n accountUserId,\n channelId,\n localTokenLoading,\n mintOnCluster,\n endpoint,\n ]);\n\n const connectWithSkill = useCallback(\n async (skill: string, model?: string, systemPrompt?: string, skillId?: string) => {\n console.log(\n '[useCdecliAutoConnect] connectWithSkill called with skill=%s skillId=%s model=%s systemPrompt=%d chars',\n skill,\n skillId ?? '',\n model,\n systemPrompt?.length ?? 0,\n );\n attemptRef.current += 1;\n const attempt = attemptRef.current;\n setStatus('connecting');\n setError(null);\n\n try {\n const token = await obtainTokenForAgent(false);\n await runConnect({ attempt, token, skill, skillId, model, systemPrompt });\n setActiveSkill(skill);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error('[useCdecliAutoConnect] connectWithSkill error:', msg);\n if (attempt !== attemptRef.current) return;\n setStatus('error');\n setError(msg);\n }\n },\n [obtainTokenForAgent, runConnect],\n );\n\n const recoverAgentAuth = useCallback(async (): Promise<boolean> => {\n attemptRef.current += 1;\n const attempt = attemptRef.current;\n try {\n const token = await obtainTokenForAgent(true);\n // A newer connect took over; it reveals the token this one reminted.\n if (attempt !== attemptRef.current) return true;\n if (!token) {\n setStatus('error');\n setError(\n mintErrorRef.current ?? 'Could not remint the CDeCLI system token. Sign in again, then retry.',\n );\n connectedRef.current = false;\n return false;\n }\n const ok = await runConnect({ attempt, token });\n if (ok && attempt === attemptRef.current) setError(null);\n return ok;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error('[useCdecliAutoConnect] recoverAgentAuth error:', msg);\n if (attempt !== attemptRef.current) return false;\n setStatus('error');\n setError(msg);\n connectedRef.current = false;\n return false;\n }\n }, [obtainTokenForAgent, runConnect]);\n\n // The same token the connect above hands the agent (a reveal of the one system token row, or the cluster mint),\n // never a fresh one: rotating it here would leave the chat's session on a revoked token.\n const obtainAgentToken = useCallback(() => obtainTokenForAgent(false), [obtainTokenForAgent]);\n\n return {\n channelConnected: status === 'connected',\n status,\n error,\n persistenceMode,\n activeSkill,\n accountId,\n connectWithSkill,\n recoverAgentAuth,\n obtainAgentToken,\n agentEndpoint: endpoint,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import {useState,useRef,useCallback,useEffect,useMemo,useSyncExternalStore}from'react';import {Platform}from'react-native';import {useCreateAccountChatFileUploadLinksMutation,useCreateAccountChatFileDownloadLinksMutation}from'common/graphql';import {markChannelHistoryRunning,useChatMutations,useChatMessages,clearChannelHistoryRunning,patchChannelHistoryPreview}from'./useChatApi.js';import {useCdecliChannel}from'./useCdecliChannel.js';import {usePrerequisiteIds}from'./usePrerequisiteIds.js';import {isAgentJwtRejected,shouldInlineGatewayMediaForHostedAgent}from'./systemTokenJwt.js';import {config}from'../config/env-config.js';import {queueGatewayMessage,MAX_QUEUED_MESSAGES,dropQueuedGatewayMessage,queuedGatewayMessages,subscribeQueuedGatewayMessages,restoreQueuedGatewayMessages,clearQueuedGatewayMessages}from'../services/queuedGatewayMessages.js';import {isImageAttachment}from'../features/attachments/isImageAttachment.js';import {buildGatewayMedia,buildGatewayMediaPreferUrls}from'../features/attachments/buildGatewayMedia.js';import {preserveAttachmentPreviews}from'../features/attachments/preserveAttachmentPreviews.js';import {releasePreviewBytes}from'../features/attachments/releasePreviewBytes.js';import {resolveAssistantReply}from'../features/chat/emptyChatResponse.js';import {splitPreResponse}from'../features/chat/preResponse.js';import {appendOrReplaceAssistant,appendFollowUpAssistant,mergeOptimisticGatewayMessages,collapseConsecutiveAssistants}from'../state/chatThreadMerge.js';var __defProp = Object.defineProperty;
1
+ import {useState,useRef,useCallback,useEffect,useMemo,useSyncExternalStore}from'react';import {Platform}from'react-native';import {useCreateAccountChatFileUploadLinksMutation,useCreateAccountChatFileDownloadLinksMutation}from'common/graphql';import {markChannelHistoryRunning,useChatMutations,useChatMessages,clearChannelHistoryRunning,patchChannelHistoryPreview}from'./useChatApi.js';import {useCdecliChannel}from'./useCdecliChannel.js';import {usePrerequisiteIds}from'./usePrerequisiteIds.js';import {isAgentJwtRejected,shouldInlineGatewayMediaForHostedAgent}from'./systemTokenJwt.js';import {config}from'../config/env-config.js';import {queueGatewayMessage,MAX_QUEUED_MESSAGES,dropQueuedGatewayMessage,queuedGatewayMessages,subscribeQueuedGatewayMessages,restoreQueuedGatewayMessages,isQueuedMessageDelivered,clearQueuedGatewayMessages}from'../services/queuedGatewayMessages.js';import {isImageAttachment}from'../features/attachments/isImageAttachment.js';import {buildGatewayMedia,buildGatewayMediaPreferUrls}from'../features/attachments/buildGatewayMedia.js';import {preserveAttachmentPreviews}from'../features/attachments/preserveAttachmentPreviews.js';import {releasePreviewBytes}from'../features/attachments/releasePreviewBytes.js';import {resolveAssistantReply}from'../features/chat/emptyChatResponse.js';import {splitPreResponse}from'../features/chat/preResponse.js';import {appendOrReplaceAssistant,appendFollowUpAssistant,mergeOptimisticGatewayMessages,collapseConsecutiveAssistants}from'../state/chatThreadMerge.js';var __defProp = Object.defineProperty;
2
2
  var __defProps = Object.defineProperties;
3
3
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
@@ -501,8 +501,19 @@ function useChatStream(sessionId, routing = {
501
501
  useEffect(() => {
502
502
  void restoreQueuedGatewayMessages();
503
503
  }, []);
504
+ const historyRef = useRef({
505
+ sessionId,
506
+ loading: backendMessagesLoading,
507
+ messages: backendMessages
508
+ });
509
+ historyRef.current = {
510
+ sessionId,
511
+ loading: backendMessagesLoading,
512
+ messages: backendMessages
513
+ };
504
514
  const flushingRef = useRef(false);
505
515
  const flushQueuedMessages = useCallback(async () => {
516
+ var _a;
506
517
  if (flushingRef.current) return;
507
518
  flushingRef.current = true;
508
519
  try {
@@ -512,8 +523,14 @@ function useChatStream(sessionId, routing = {
512
523
  if (!rt.channelConnected || !sid) return;
513
524
  const next = queuedGatewayMessages(sid, rt.accountId)[0];
514
525
  if (!next) return;
515
- const outcome = await sendMessageRef.current(next.text, next.attachments, next.channelId, false, next.id);
516
- if (outcome !== "sent") return;
526
+ const history = historyRef.current;
527
+ if (history.sessionId !== sid || history.loading) return;
528
+ if (isQueuedMessageDelivered(next, (_a = history.messages) != null ? _a : [])) {
529
+ dropQueuedGatewayMessage(next.id);
530
+ } else {
531
+ const outcome = await sendMessageRef.current(next.text, next.attachments, next.channelId, false, next.id);
532
+ if (outcome !== "sent") return;
533
+ }
517
534
  }
518
535
  } finally {
519
536
  flushingRef.current = false;
@@ -527,7 +544,7 @@ function useChatStream(sessionId, routing = {
527
544
  if (!connected || !queuedChannelId) return;
528
545
  if (!reconnected && queuedGatewayMessages(queuedChannelId, routing.accountId).length === 0) return;
529
546
  void flushQueuedMessages();
530
- }, [routing.channelConnected, routing.accountId, queuedChannelId, flushQueuedMessages]);
547
+ }, [routing.channelConnected, routing.accountId, queuedChannelId, flushQueuedMessages, backendMessagesLoading]);
531
548
  const discardQueuedMessages = useCallback(() => {
532
549
  if (!queuedChannelId) return;
533
550
  clearQueuedGatewayMessages(queuedChannelId);