@ateam-ai-mobile/core 0.1.3 → 0.1.5

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@ateam-ai-mobile/core",
3
- "version": "0.1.3",
4
- "description": "Headless ADAS SDK core \u2014 auth bridge, API client, chat/job streaming, MCP calls, theme tokens. No UI, no Expo dependencies.",
3
+ "version": "0.1.5",
4
+ "description": "Headless ADAS SDK core — auth bridge, API client, chat/job streaming, MCP calls, theme tokens. No UI, no Expo dependencies.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
@@ -24,4 +24,4 @@
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  }
27
- }
27
+ }
@@ -1,4 +1,3 @@
1
- import { apiFetch } from './client';
2
1
  import { getConsumerAuthApiUrl, getToken, getConfig, type AdasSession } from '../config';
3
2
  import { useChatStore, closeChatStream } from '../chat/store';
4
3
 
@@ -17,8 +16,22 @@ export interface TenantMembership {
17
16
  * leaks the full tenant list. Returns [] when signed out or unknown.
18
17
  */
19
18
  export async function listMyTenants(): Promise<TenantMembership[]> {
20
- const data = await apiFetch<{ ok: boolean; tenants?: TenantMembership[] }>('/api/tenants/list');
21
- return data.ok && Array.isArray(data.tenants) ? data.tenants : [];
19
+ const token = getToken();
20
+ if (!token) return [];
21
+ try {
22
+ const res = await fetch(`${getConsumerAuthApiUrl()}/platform/consumer/my-tenants`, {
23
+ headers: { Authorization: `Bearer ${token}` },
24
+ });
25
+ const data = await res.json().catch(() => null);
26
+ if (!res.ok || !data?.ok || !Array.isArray(data.tenants)) return [];
27
+ return data.tenants.map((t: any) => ({
28
+ id: t.id,
29
+ name: t.name || t.id,
30
+ role: t.role ?? null,
31
+ }));
32
+ } catch {
33
+ return [];
34
+ }
22
35
  }
23
36
 
24
37
  /**
package/src/chat/store.ts CHANGED
@@ -353,85 +353,105 @@ export const useChatStore = create<ChatState>((set, get) => ({
353
353
 
354
354
  let firstRunning: Message | undefined = keptPlaceholders.find(m => m.jobProgress?.status === 'running');
355
355
 
356
- // Cold-start / post-refresh reattach: the WS subscription dies on app
357
- // restart. /api/chain/current is the single hydration endpoint that
358
- // says "is a chain still in flight for this actor?". If yes, re-open
359
- // the live subscription on the same chainId so the pill comes back.
356
+ // Cold-start / post-refresh reattach: WS subscriptions die on app
357
+ // restart. Fetch ALL active chains for this actor (chain-native SlimChain
358
+ // API) and reattach EACH — multiple concurrent chains must all come back
359
+ // in progress, not just the most-recently-updated one. Falls back to the
360
+ // legacy single-chain /api/chain/current on older backends.
360
361
  try {
361
- const chainRes = await fetch(`${getCoreApiUrl()}/api/chain/current`, {
362
+ let activeChains: any[] = [];
363
+ const chainsRes = await fetch(`${getCoreApiUrl()}/api/chains/active`, {
362
364
  headers: coreHeaders(),
363
365
  });
364
- if (chainRes.ok) {
365
- const chainBody = await chainRes.json();
366
- const slim = chainBody?.data;
367
- if (slim?.id) {
368
- const chainId = String(slim.id);
369
- const alreadyTracked = finalMessages.some(m =>
370
- m.role === 'assistant' && m.jobProgress?.jobId === chainId
371
- );
372
- if (!alreadyTracked) {
373
- const progress = snapshotToProgress(slim, chainId);
374
- const chainCreatedAt = typeof slim.createdAt === 'number'
375
- ? slim.createdAt
376
- : (typeof slim.lastUpdate === 'number' ? slim.lastUpdate : Date.now());
377
- const chainLastUpdate = typeof slim.lastUpdate === 'number' ? slim.lastUpdate : chainCreatedAt;
378
- const jobProgress = {
379
- jobId: chainId,
380
- actorId: extractActorId(slim),
381
- status: 'running',
382
- chainStartedAt: chainCreatedAt,
383
- ...progress,
384
- } as JobProgress;
385
-
386
- // Synthesize the user bubble if a hard refresh wiped the
387
- // optimistic one and the chain hasn't persisted its turn yet.
388
- const goal = typeof slim.goal === 'string' ? slim.goal : '';
389
- const userExists = finalMessages.some(m =>
390
- m.role === 'user' && (m.chainId === chainId || (goal && m.content === goal))
391
- );
392
- if (goal && !userExists) {
393
- finalMessages.push({
394
- id: `user-${chainId}`,
395
- role: 'user',
396
- content: goal,
397
- timestamp: new Date(chainCreatedAt),
398
- chainId,
399
- });
400
- }
401
-
402
- // Assistant side: attach to matching persisted turn if one
403
- // exists; otherwise push a placeholder bubble.
404
- let matchIdx = -1;
405
- for (let i = finalMessages.length - 1; i >= 0; i--) {
406
- const m = finalMessages[i];
407
- if (m.role === 'assistant' && m.chainId === chainId) {
408
- matchIdx = i;
409
- break;
410
- }
411
- }
412
- if (matchIdx >= 0) {
413
- finalMessages[matchIdx] = { ...finalMessages[matchIdx], jobProgress };
414
- if (jobProgress.status === 'running') firstRunning = finalMessages[matchIdx];
415
- } else {
416
- const reattachBubble: Message = {
417
- id: `job-${chainId}`,
418
- role: 'assistant',
419
- content: 'Working on it...',
420
- timestamp: new Date(chainLastUpdate),
421
- jobProgress,
422
- chainId,
423
- };
424
- finalMessages.push(reattachBubble);
425
- if (jobProgress.status === 'running') firstRunning = reattachBubble;
426
- }
427
-
428
- finalMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
429
-
430
- // Re-open the live subscription.
431
- streamJob(chainId, getTenant(), getToken());
366
+ if (chainsRes.ok) {
367
+ const body = await chainsRes.json();
368
+ activeChains = Array.isArray(body?.chains) ? body.chains : [];
369
+ } else {
370
+ // Legacy backend without the plural endpoint — single most-recent chain.
371
+ const legacy = await fetch(`${getCoreApiUrl()}/api/chain/current`, {
372
+ headers: coreHeaders(),
373
+ });
374
+ if (legacy.ok) {
375
+ const lb = await legacy.json();
376
+ if (lb?.data?.id) activeChains = [lb.data];
377
+ }
378
+ }
379
+
380
+ for (const slim of activeChains) {
381
+ if (!slim?.id) continue;
382
+ const chainId = String(slim.id);
383
+ const alreadyTracked = finalMessages.some(m =>
384
+ m.role === 'assistant' && m.jobProgress?.jobId === chainId
385
+ );
386
+ if (alreadyTracked) continue;
387
+
388
+ // Chain-native status: prefer the whole-chain aggregate (chainStatus)
389
+ // over the active job's per-job status, so e.g. a chain still running
390
+ // across a handoff isn't shown as "done". A pending question still
391
+ // overrides to "paused" inside snapshotToProgress.
392
+ const progress = snapshotToProgress(
393
+ { ...slim, status: slim.chainStatus || slim.status },
394
+ chainId,
395
+ );
396
+ const chainCreatedAt = typeof slim.createdAt === 'number'
397
+ ? slim.createdAt
398
+ : (typeof slim.lastUpdate === 'number' ? slim.lastUpdate : Date.now());
399
+ const chainLastUpdate = typeof slim.lastUpdate === 'number' ? slim.lastUpdate : chainCreatedAt;
400
+ const jobProgress = {
401
+ jobId: chainId,
402
+ actorId: extractActorId(slim),
403
+ status: 'running',
404
+ chainStartedAt: chainCreatedAt,
405
+ ...progress,
406
+ } as JobProgress;
407
+
408
+ // Synthesize the user bubble if a hard refresh wiped the optimistic
409
+ // one and the chain hasn't persisted its turn yet.
410
+ const goal = typeof slim.goal === 'string' ? slim.goal : '';
411
+ const userExists = finalMessages.some(m =>
412
+ m.role === 'user' && (m.chainId === chainId || (goal && m.content === goal))
413
+ );
414
+ if (goal && !userExists) {
415
+ finalMessages.push({
416
+ id: `user-${chainId}`,
417
+ role: 'user',
418
+ content: goal,
419
+ timestamp: new Date(chainCreatedAt),
420
+ chainId,
421
+ });
422
+ }
423
+
424
+ // Assistant side: attach to matching persisted turn if one exists;
425
+ // otherwise push a placeholder bubble.
426
+ let matchIdx = -1;
427
+ for (let i = finalMessages.length - 1; i >= 0; i--) {
428
+ const m = finalMessages[i];
429
+ if (m.role === 'assistant' && m.chainId === chainId) {
430
+ matchIdx = i;
431
+ break;
432
432
  }
433
433
  }
434
+ if (matchIdx >= 0) {
435
+ finalMessages[matchIdx] = { ...finalMessages[matchIdx], jobProgress };
436
+ if (jobProgress.status === 'running' && !firstRunning) firstRunning = finalMessages[matchIdx];
437
+ } else {
438
+ const reattachBubble: Message = {
439
+ id: `job-${chainId}`,
440
+ role: 'assistant',
441
+ content: 'Working on it...',
442
+ timestamp: new Date(chainLastUpdate),
443
+ jobProgress,
444
+ chainId,
445
+ };
446
+ finalMessages.push(reattachBubble);
447
+ if (jobProgress.status === 'running' && !firstRunning) firstRunning = reattachBubble;
448
+ }
449
+
450
+ // Re-open the live subscription for this chain.
451
+ streamJob(chainId, getTenant(), getToken());
434
452
  }
453
+
454
+ finalMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
435
455
  } catch {}
436
456
 
437
457
  set({
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export {
27
27
  getTenant,
28
28
  getUser,
29
29
  getToken,
30
+ refreshToken,
30
31
  } from './config';
31
32
  export type { AdasConfig, TokenProvider, RefreshOutcome, AdasUser, AdasSession } from './config';
32
33