@ateam-ai-mobile/core 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,7 +1,7 @@
1
1
  {
2
2
  "name": "@ateam-ai-mobile/core",
3
- "version": "0.1.2",
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.4",
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
@@ -128,6 +128,10 @@ function snapshotToProgress(snap: any, rootJobId: string): Partial<JobProgress>
128
128
  const liveProgress = extractLiveProgress(snap);
129
129
  if (liveProgress) progress.liveProgress = liveProgress;
130
130
 
131
+ // Human-language status (additive; absent when disabled/uncached).
132
+ if (typeof snap?.humanStatus === 'string' && snap.humanStatus) progress.humanStatus = snap.humanStatus;
133
+ if (snap?.humanLabels && typeof snap.humanLabels === 'object') progress.humanLabels = snap.humanLabels;
134
+
131
135
  if (pq) {
132
136
  progress.pendingQuestion = {
133
137
  inputId: pq.correlation_id || pq.correlationId || pq.inputId || '',
@@ -349,85 +353,105 @@ export const useChatStore = create<ChatState>((set, get) => ({
349
353
 
350
354
  let firstRunning: Message | undefined = keptPlaceholders.find(m => m.jobProgress?.status === 'running');
351
355
 
352
- // Cold-start / post-refresh reattach: the WS subscription dies on app
353
- // restart. /api/chain/current is the single hydration endpoint that
354
- // says "is a chain still in flight for this actor?". If yes, re-open
355
- // 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.
356
361
  try {
357
- const chainRes = await fetch(`${getCoreApiUrl()}/api/chain/current`, {
362
+ let activeChains: any[] = [];
363
+ const chainsRes = await fetch(`${getCoreApiUrl()}/api/chains/active`, {
358
364
  headers: coreHeaders(),
359
365
  });
360
- if (chainRes.ok) {
361
- const chainBody = await chainRes.json();
362
- const slim = chainBody?.data;
363
- if (slim?.id) {
364
- const chainId = String(slim.id);
365
- const alreadyTracked = finalMessages.some(m =>
366
- m.role === 'assistant' && m.jobProgress?.jobId === chainId
367
- );
368
- if (!alreadyTracked) {
369
- const progress = snapshotToProgress(slim, chainId);
370
- const chainCreatedAt = typeof slim.createdAt === 'number'
371
- ? slim.createdAt
372
- : (typeof slim.lastUpdate === 'number' ? slim.lastUpdate : Date.now());
373
- const chainLastUpdate = typeof slim.lastUpdate === 'number' ? slim.lastUpdate : chainCreatedAt;
374
- const jobProgress = {
375
- jobId: chainId,
376
- actorId: extractActorId(slim),
377
- status: 'running',
378
- chainStartedAt: chainCreatedAt,
379
- ...progress,
380
- } as JobProgress;
381
-
382
- // Synthesize the user bubble if a hard refresh wiped the
383
- // optimistic one and the chain hasn't persisted its turn yet.
384
- const goal = typeof slim.goal === 'string' ? slim.goal : '';
385
- const userExists = finalMessages.some(m =>
386
- m.role === 'user' && (m.chainId === chainId || (goal && m.content === goal))
387
- );
388
- if (goal && !userExists) {
389
- finalMessages.push({
390
- id: `user-${chainId}`,
391
- role: 'user',
392
- content: goal,
393
- timestamp: new Date(chainCreatedAt),
394
- chainId,
395
- });
396
- }
397
-
398
- // Assistant side: attach to matching persisted turn if one
399
- // exists; otherwise push a placeholder bubble.
400
- let matchIdx = -1;
401
- for (let i = finalMessages.length - 1; i >= 0; i--) {
402
- const m = finalMessages[i];
403
- if (m.role === 'assistant' && m.chainId === chainId) {
404
- matchIdx = i;
405
- break;
406
- }
407
- }
408
- if (matchIdx >= 0) {
409
- finalMessages[matchIdx] = { ...finalMessages[matchIdx], jobProgress };
410
- if (jobProgress.status === 'running') firstRunning = finalMessages[matchIdx];
411
- } else {
412
- const reattachBubble: Message = {
413
- id: `job-${chainId}`,
414
- role: 'assistant',
415
- content: 'Working on it...',
416
- timestamp: new Date(chainLastUpdate),
417
- jobProgress,
418
- chainId,
419
- };
420
- finalMessages.push(reattachBubble);
421
- if (jobProgress.status === 'running') firstRunning = reattachBubble;
422
- }
423
-
424
- finalMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
425
-
426
- // Re-open the live subscription.
427
- 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;
428
432
  }
429
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());
430
452
  }
453
+
454
+ finalMessages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
431
455
  } catch {}
432
456
 
433
457
  set({
package/src/chat/types.ts CHANGED
@@ -26,6 +26,12 @@ export interface JobProgress {
26
26
  resultActions?: UIAction[];
27
27
  /** Live progress from adas_emit_progress() — latest event from a running fat tool */
28
28
  liveProgress?: { message: string; step?: number; total?: number };
29
+ /** One warm, present-tense, localized line describing the current action
30
+ * (backend statusI18n). Absent when disabled / not yet cached → fall back
31
+ * to raw tokens. */
32
+ humanStatus?: string;
33
+ /** Lookup { rawToken → friendly phrase } for tokens in this snapshot. */
34
+ humanLabels?: Record<string, string>;
29
35
  pendingQuestion?: {
30
36
  inputId: string;
31
37
  question: string;
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