openmates 0.27.0-alpha.5 → 0.27.0-alpha.7

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.
@@ -12586,7 +12586,7 @@ var OpenMatesClient = class _OpenMatesClient {
12586
12586
  if (!userId) {
12587
12587
  throw new Error("Could not resolve current user id for local connected-account connector.");
12588
12588
  }
12589
- const { createHash: createHash31 } = await import("crypto");
12589
+ const { createHash: createHash32 } = await import("crypto");
12590
12590
  const accountId = randomUUID10();
12591
12591
  const masterKey = this.getMasterKeyBytes();
12592
12592
  const encryptLocalConnectorValue = async (value) => {
@@ -12595,9 +12595,9 @@ var OpenMatesClient = class _OpenMatesClient {
12595
12595
  };
12596
12596
  const payload = {
12597
12597
  id: accountId,
12598
- hashed_user_id: createHash31("sha256").update(userId).digest("hex"),
12598
+ hashed_user_id: createHash32("sha256").update(userId).digest("hex"),
12599
12599
  encrypted_provider_type: await encryptLocalConnectorValue(input.provider_id),
12600
- provider_type_hash: createHash31("sha256").update(input.provider_id).digest("hex"),
12600
+ provider_type_hash: createHash32("sha256").update(input.provider_id).digest("hex"),
12601
12601
  encrypted_account_label: await encryptLocalConnectorValue(input.label),
12602
12602
  encrypted_capabilities: await encryptLocalConnectorValue(input.capabilities),
12603
12603
  encrypted_app_permissions: await encryptLocalConnectorValue({
@@ -13388,7 +13388,61 @@ var OpenMatesClient = class _OpenMatesClient {
13388
13388
  createdAt: typeof d.created_at === "number" ? d.created_at : null,
13389
13389
  updatedAt: typeof d.last_edited_overall_timestamp === "number" ? d.last_edited_overall_timestamp : null,
13390
13390
  category,
13391
- mateName: category ? MATE_NAMES[category] ?? null : null
13391
+ mateName: category ? MATE_NAMES[category] ?? null : null,
13392
+ parentId: typeof d.parent_id === "string" ? d.parent_id : null,
13393
+ isSubChat: d.is_sub_chat === true,
13394
+ isHiddenCandidate: !chatKeyBytes
13395
+ };
13396
+ }
13397
+ /** Global activity snapshot; decrypt only running chats and their ancestors. */
13398
+ async getChatActivity(options = {}) {
13399
+ this.requireSession();
13400
+ const teamId = this.resolveTeamContext(options), master = Buffer.from(this.getMasterKeyBytes());
13401
+ const response = await this.http.get(
13402
+ this.appendTeamQuery("/v1/chats/activity", options),
13403
+ this.getCliRequestHeaders()
13404
+ );
13405
+ if (!response.ok) throw new Error(`Chat activity unavailable: HTTP ${response.status}`);
13406
+ if (!this.hasSession() || teamId !== this.resolveTeamContext(options) || !master.equals(Buffer.from(this.getMasterKeyBytes()))) throw new Error("Chat workspace changed");
13407
+ const ids = response.data.active_tasks.map((task) => task.chat_id);
13408
+ if (!ids.length) return { ids, chats: [] };
13409
+ const chats = await this.getSidebarChats(response.data.chats.map((chat) => chat.chat_id), options);
13410
+ if (teamId !== this.resolveTeamContext(options)) throw new Error("Chat workspace changed");
13411
+ return { ids, chats };
13412
+ }
13413
+ /** Minimal encrypted metadata; unreadable keys stay excluded from normal surfaces. */
13414
+ async getSidebarChats(ids, options = {}) {
13415
+ this.requireSession();
13416
+ const teamId = this.resolveTeamContext(options), master = Buffer.from(this.getMasterKeyBytes());
13417
+ const key = await this.getChatWrappingKey(teamId, master), result = [];
13418
+ const current = () => this.hasSession() && teamId === this.resolveTeamContext(options) && master.equals(Buffer.from(this.getMasterKeyBytes()));
13419
+ for (let start = 0; start < ids.length; start += 100) {
13420
+ if (!current()) throw new Error("Chat workspace changed");
13421
+ const response = await this.http.post(
13422
+ this.appendTeamQuery("/v1/chats/metadata/batch", options),
13423
+ { chat_ids: ids.slice(start, start + 100) },
13424
+ this.getCliRequestHeaders()
13425
+ );
13426
+ if (!response.ok) throw new Error(`Sidebar metadata unavailable: HTTP ${response.status}`);
13427
+ for (const details of response.data.chats) {
13428
+ const timestamp = Number(details.updated_at);
13429
+ result.push(await this.decryptChatListItem({ details: {
13430
+ ...details,
13431
+ last_edited_overall_timestamp: Number.isFinite(timestamp) ? timestamp : Date.parse(String(details.updated_at)) / 1e3
13432
+ }, messages: [] }, key, null, teamId));
13433
+ }
13434
+ }
13435
+ if (!current()) throw new Error("Chat workspace changed");
13436
+ return result;
13437
+ }
13438
+ /** Observe lifecycle metadata without claiming or collecting AI responses. */
13439
+ async observeChatActivity(onChange) {
13440
+ const { ws } = await this.openWsClient({ taskUpdateJobs: false });
13441
+ const types = ["ai_task_initiated", "ai_typing_started", "ai_typing_ended", "ai_background_response_completed", "ai_task_cancel_requested", "ai_task_error", "pending_ai_response"];
13442
+ const cleanup = types.map((type) => ws.onMessageType(type, onChange));
13443
+ return () => {
13444
+ cleanup.forEach((remove) => remove());
13445
+ ws.close();
13392
13446
  };
13393
13447
  }
13394
13448
  async listChats(limit = 10, page = 1, options = {}) {
@@ -14391,8 +14445,8 @@ var OpenMatesClient = class _OpenMatesClient {
14391
14445
  );
14392
14446
  }
14393
14447
  const embedId = String(embed.embed_id ?? embed.id ?? "");
14394
- const { createHash: createHash31 } = await import("crypto");
14395
- const hashedEmbedId = createHash31("sha256").update(embedId).digest("hex");
14448
+ const { createHash: createHash32 } = await import("crypto");
14449
+ const hashedEmbedId = createHash32("sha256").update(embedId).digest("hex");
14396
14450
  const embedKeyBytes = await this.resolveEmbedKey(
14397
14451
  cache,
14398
14452
  masterKey,
@@ -14494,7 +14548,7 @@ var OpenMatesClient = class _OpenMatesClient {
14494
14548
  async resolveEmbedKey(cache, masterKey, embed, embedId, hashedEmbedId, visited = /* @__PURE__ */ new Set()) {
14495
14549
  if (visited.has(embedId)) return null;
14496
14550
  visited.add(embedId);
14497
- const { createHash: createHash31 } = await import("crypto");
14551
+ const { createHash: createHash32 } = await import("crypto");
14498
14552
  const masterKeyEntry = cache.embedKeys.find(
14499
14553
  (ek) => ek.hashed_embed_id === hashedEmbedId && String(ek.key_type) === "master"
14500
14554
  );
@@ -14511,7 +14565,7 @@ var OpenMatesClient = class _OpenMatesClient {
14511
14565
  if (chatKeyEntry && typeof chatKeyEntry.encrypted_embed_key === "string") {
14512
14566
  const hashedChatId = String(chatKeyEntry.hashed_chat_id ?? "");
14513
14567
  const owningChat = cache.chats.find((c) => {
14514
- const chatHash = createHash31("sha256").update(String(c.details.id ?? "")).digest("hex");
14568
+ const chatHash = createHash32("sha256").update(String(c.details.id ?? "")).digest("hex");
14515
14569
  return chatHash === hashedChatId;
14516
14570
  });
14517
14571
  if (owningChat) {
@@ -14540,7 +14594,7 @@ var OpenMatesClient = class _OpenMatesClient {
14540
14594
  const parentFullId = String(
14541
14595
  parentEmbed2.embed_id ?? parentEmbed2.id ?? ""
14542
14596
  );
14543
- const parentHashedId = createHash31("sha256").update(parentFullId).digest("hex");
14597
+ const parentHashedId = createHash32("sha256").update(parentFullId).digest("hex");
14544
14598
  const parentKey = await this.resolveEmbedKey(
14545
14599
  cache,
14546
14600
  masterKey,
@@ -17357,7 +17411,7 @@ var OpenMatesClient = class _OpenMatesClient {
17357
17411
  this.requireSession();
17358
17412
  const response = await this.http.post(
17359
17413
  "/v1/projects/ask/plan",
17360
- { instruction: input.instruction },
17414
+ { instruction: input.instruction, ...input.chatTitles !== void 0 ? { chat_titles: input.chatTitles } : {} },
17361
17415
  this.getCliRequestHeaders()
17362
17416
  );
17363
17417
  if (!response.ok) throw new Error(`Project ask planning failed with HTTP ${response.status}`);
@@ -17366,7 +17420,7 @@ var OpenMatesClient = class _OpenMatesClient {
17366
17420
  async listProjectItems(projectId, options = {}) {
17367
17421
  this.requireSession();
17368
17422
  const response = await this.http.get(
17369
- this.appendTeamQuery(`/v1/projects/${encodeURIComponent(projectId)}/items`, options),
17423
+ this.appendTeamQuery(`/v1/projects/${encodeURIComponent(projectId)}/items${options.chatOnly ? "?chat_only=true" : ""}`, options),
17370
17424
  this.getCliRequestHeaders()
17371
17425
  );
17372
17426
  if (!response.ok) throw this.projectRequestError("item list", response);
@@ -17503,10 +17557,10 @@ var OpenMatesClient = class _OpenMatesClient {
17503
17557
  }
17504
17558
  return { ws: opened.ws, ownerId: opened.ownerId };
17505
17559
  }
17506
- async createProjectItem(projectId, input) {
17560
+ async createProjectItem(projectId, input, options = {}) {
17507
17561
  this.requireSession();
17508
17562
  const response = await this.http.post(
17509
- `/v1/projects/${encodeURIComponent(projectId)}/items`,
17563
+ this.appendTeamQuery(`/v1/projects/${encodeURIComponent(projectId)}/items`, options),
17510
17564
  input,
17511
17565
  this.getCliRequestHeaders()
17512
17566
  );
@@ -17515,6 +17569,20 @@ var OpenMatesClient = class _OpenMatesClient {
17515
17569
  }
17516
17570
  return response.data.item;
17517
17571
  }
17572
+ async moveProjectItemToFolder(projectId, itemId, folderId, options = {}) {
17573
+ this.requireSession();
17574
+ const response = await this.http.patch(
17575
+ this.appendTeamQuery(`/v1/projects/${encodeURIComponent(projectId)}/items/${encodeURIComponent(itemId)}`, options),
17576
+ { folder_id: folderId, updated_at: Math.floor(Date.now() / 1e3) },
17577
+ this.getCliRequestHeaders()
17578
+ );
17579
+ if (!response.ok) throw this.projectRequestError("folder move", response);
17580
+ }
17581
+ async createProjectFolder(projectId, input, options = {}) {
17582
+ this.requireSession();
17583
+ const response = await this.http.post(this.appendTeamQuery(`/v1/projects/${encodeURIComponent(projectId)}/folders`, options), input, this.getCliRequestHeaders());
17584
+ if (!response.ok) throw this.projectRequestError("folder create", response);
17585
+ }
17518
17586
  async deleteProjectItemByTarget(projectId, itemType, targetId, options = {}) {
17519
17587
  this.requireSession();
17520
17588
  const params = new URLSearchParams({ item_type: itemType, target_id: targetId });
@@ -19150,7 +19218,7 @@ Required: ${schema.required.join(", ")}`
19150
19218
  const session = this.requireSession();
19151
19219
  const masterKey = base64ToBytes(session.masterKeyExportedB64);
19152
19220
  const cache = await this.ensureSynced();
19153
- const { createHash: createHash31 } = await import("crypto");
19221
+ const { createHash: createHash32 } = await import("crypto");
19154
19222
  const embed = cache.embeds.find(
19155
19223
  (e) => String(e.embed_id ?? "").startsWith(embedIdOrShort) || String(e.id ?? "").startsWith(embedIdOrShort)
19156
19224
  );
@@ -19158,7 +19226,7 @@ Required: ${schema.required.join(", ")}`
19158
19226
  throw new Error(`Embed '${embedIdOrShort}' not found in local cache.`);
19159
19227
  }
19160
19228
  const embedId = String(embed.embed_id ?? embed.id ?? "");
19161
- const hashedEmbedId = createHash31("sha256").update(embedId).digest("hex");
19229
+ const hashedEmbedId = createHash32("sha256").update(embedId).digest("hex");
19162
19230
  const embedKeyBytes = await this.resolveEmbedKey(
19163
19231
  cache,
19164
19232
  masterKey,
@@ -19266,8 +19334,8 @@ Required: ${schema.required.join(", ")}`
19266
19334
  if (!embed) {
19267
19335
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
19268
19336
  }
19269
- const { createHash: createHash31 } = await import("crypto");
19270
- const hashedEmbedId = createHash31("sha256").update(embedId).digest("hex");
19337
+ const { createHash: createHash32 } = await import("crypto");
19338
+ const hashedEmbedId = createHash32("sha256").update(embedId).digest("hex");
19271
19339
  const embedKey = await this.resolveEmbedKey(
19272
19340
  cache,
19273
19341
  masterKey,
@@ -19383,8 +19451,8 @@ Required: ${schema.required.join(", ")}`
19383
19451
  if (!embed) {
19384
19452
  throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
19385
19453
  }
19386
- const { createHash: createHash31 } = await import("crypto");
19387
- const hashedEmbedId = createHash31("sha256").update(embedId).digest("hex");
19454
+ const { createHash: createHash32 } = await import("crypto");
19455
+ const hashedEmbedId = createHash32("sha256").update(embedId).digest("hex");
19388
19456
  const embedKey = await this.resolveEmbedKey(
19389
19457
  cache,
19390
19458
  masterKey,
@@ -19916,9 +19984,9 @@ Required: ${schema.required.join(", ")}`
19916
19984
  chats.splice(0, chats.length, ...reconciledChats);
19917
19985
  if (totalChatCount === 0) totalChatCount = chats.length;
19918
19986
  if (reconciliation.deleted_chat_ids?.length) {
19919
- const { createHash: createHash31 } = await import("crypto");
19987
+ const { createHash: createHash32 } = await import("crypto");
19920
19988
  const deletedHashes = new Set(
19921
- reconciliation.deleted_chat_ids.map((id) => createHash31("sha256").update(id).digest("hex"))
19989
+ reconciliation.deleted_chat_ids.map((id) => createHash32("sha256").update(id).digest("hex"))
19922
19990
  );
19923
19991
  const keptEmbeds = embeds.filter((embed) => !deletedHashes.has(String(embed.hashed_chat_id ?? "")));
19924
19992
  const keptKeys = embedKeys.filter((key) => !deletedHashes.has(String(key.hashed_chat_id ?? "")));
@@ -31158,7 +31226,7 @@ async function flushPendingTaskActivities(client2, teamId, stateDir2 = resolveSt
31158
31226
  import { existsSync as existsSync26, readFileSync as readFileSync24, realpathSync as realpathSync12, readdirSync as readdirSync14, statSync as statSync11, writeFileSync as writeFileSync16 } from "fs";
31159
31227
  import { fileURLToPath as fileURLToPath4 } from "url";
31160
31228
  import { basename as basename9, dirname as dirname16, join as join29 } from "path";
31161
- import { createHash as createHash30, randomBytes as randomBytes14, randomUUID as randomUUID23 } from "crypto";
31229
+ import { createHash as createHash31, randomBytes as randomBytes14, randomUUID as randomUUID24 } from "crypto";
31162
31230
  import { arch as arch2, platform as platform3 } from "os";
31163
31231
  import { parse as parseYaml, parseDocument as parseYamlDocument, stringify as stringifyYaml } from "yaml";
31164
31232
 
@@ -62135,6 +62203,64 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
62135
62203
  }
62136
62204
  },
62137
62205
  chats: {
62206
+ projects: {
62207
+ move: {
62208
+ text: "Move to project"
62209
+ },
62210
+ move_here: {
62211
+ text: "Move here"
62212
+ },
62213
+ add: {
62214
+ text: "Add to project"
62215
+ },
62216
+ create: {
62217
+ text: "Create new project"
62218
+ },
62219
+ add_here: {
62220
+ text: "Add here"
62221
+ },
62222
+ new_folder: {
62223
+ text: "New subfolder"
62224
+ },
62225
+ folder_name: {
62226
+ text: "Folder name"
62227
+ },
62228
+ path: {
62229
+ text: "Project path"
62230
+ },
62231
+ up: {
62232
+ text: "Go up one level"
62233
+ },
62234
+ ancestors: {
62235
+ text: "Show full path"
62236
+ },
62237
+ open: {
62238
+ text: "Open project"
62239
+ },
62240
+ organizing: {
62241
+ text: "Organizing chats\u2026"
62242
+ },
62243
+ error: {
62244
+ text: "Could not organize chats. Please try again."
62245
+ }
62246
+ },
62247
+ activity: {
62248
+ heading: {
62249
+ text: "Active chats"
62250
+ },
62251
+ count: {
62252
+ text: "{count} chats active\u2026"
62253
+ },
62254
+ count_single: {
62255
+ text: "1 chat active\u2026"
62256
+ },
62257
+ subchats: {
62258
+ text: "{count} subchats running"
62259
+ },
62260
+ subchats_single: {
62261
+ text: "1 subchat running"
62262
+ }
62263
+ },
62138
62264
  chat: {
62139
62265
  welcome: {
62140
62266
  back_to_recent: {
@@ -83994,7 +84120,7 @@ function formatMetricDelta(delta, suffix) {
83994
84120
  // src/tui.ts
83995
84121
  import { createInterface as createInterface4 } from "readline/promises";
83996
84122
  import { stdin as nodeStdin, stdout as nodeStdout } from "process";
83997
- import { randomUUID as randomUUID20 } from "crypto";
84123
+ import { randomUUID as randomUUID21 } from "crypto";
83998
84124
 
83999
84125
  // src/tuiExampleContinuation.ts
84000
84126
  function buildExampleContinuationHistory(conversation) {
@@ -84546,6 +84672,21 @@ function padCells(value, width) {
84546
84672
  const trimmed = truncateCells(value, width);
84547
84673
  return trimmed + " ".repeat(Math.max(0, width - cells(trimmed)));
84548
84674
  }
84675
+ function sliceCells(value, start, width) {
84676
+ start = Math.max(0, Math.floor(start));
84677
+ width = Math.max(0, Math.floor(width));
84678
+ const end = start + width;
84679
+ let position = 0, result = "";
84680
+ for (const { segment } of segments.segment(terminalText(value).replace(/\n/g, " "))) {
84681
+ const size = graphemeWidth(segment), next = position + size;
84682
+ if (next > start && position < end) {
84683
+ result += position >= start && next <= end ? segment : " ".repeat(Math.min(end, next) - Math.max(start, position));
84684
+ }
84685
+ position = next;
84686
+ if (position >= end) break;
84687
+ }
84688
+ return result + " ".repeat(Math.max(0, width - cells(result)));
84689
+ }
84549
84690
  function wrapCells(value, width) {
84550
84691
  width = Math.max(1, width);
84551
84692
  const result = [];
@@ -84672,13 +84813,13 @@ function storedPath(metadata, displayName) {
84672
84813
  return null;
84673
84814
  }
84674
84815
  }
84675
- async function loadTuiProject(client2, id) {
84816
+ async function loadTuiProject(client2, id, chatOnly = false) {
84676
84817
  const context = contextFor(client2);
84677
84818
  const detail = await client2.getProject(id, context);
84678
84819
  const project = await projectFromRecord(client2, detail.project, context);
84679
84820
  const [itemsResponse, sourceRecords] = await Promise.all([
84680
- client2.listProjectItems(id, context),
84681
- client2.listProjectSources(id, context)
84821
+ client2.listProjectItems(id, { ...context, chatOnly }),
84822
+ chatOnly ? Promise.resolve([]) : client2.listProjectSources(id, context)
84682
84823
  ]);
84683
84824
  const folderIds = itemsResponse.folders.map((folder) => String(folder.folder_id ?? "")).filter(Boolean);
84684
84825
  const folderByHash = new Map(folderIds.map((folderId) => [createHash21("sha256").update(folderId).digest("hex"), folderId]));
@@ -84912,7 +85053,7 @@ function buildProjectForm() {
84912
85053
  ]
84913
85054
  };
84914
85055
  }
84915
- async function submitProjectForm(client2, form) {
85056
+ async function submitProjectForm(client2, form, chatOrganizationOnly = false) {
84916
85057
  const name = formValue(form, "name").trim();
84917
85058
  if (!name) throw new Error("Project name is required.");
84918
85059
  const writeMode = formValue(form, "write_mode");
@@ -84936,7 +85077,7 @@ async function submitProjectForm(client2, form) {
84936
85077
  created_at: timestamp,
84937
85078
  updated_at: timestamp,
84938
85079
  last_opened_at: timestamp,
84939
- write_mode: writeMode,
85080
+ write_mode: chatOrganizationOnly ? null : writeMode,
84940
85081
  default_focus_id: focus.focus_id,
84941
85082
  encrypted_settings: await encryptWithAesGcmCombined(JSON.stringify({ default_focus: focus }), key),
84942
85083
  key_wrappers: wrapping.teamId ? [{
@@ -84948,7 +85089,7 @@ async function submitProjectForm(client2, form) {
84948
85089
  created_at: timestamp
84949
85090
  }] : []
84950
85091
  };
84951
- const result = await client2.createProject(payload, context);
85092
+ const result = await client2.createProject({ ...payload, ...chatOrganizationOnly ? { chat_organization_only: true } : {} }, context);
84952
85093
  return projectFromRecord(client2, { ...payload, ...result.project ?? {} }, context);
84953
85094
  }
84954
85095
 
@@ -85523,6 +85664,10 @@ var TUI_ACTIONS = [
85523
85664
  { label: "Move selected task", command: "/task-status" },
85524
85665
  { label: "Add task activity", command: "/task-activity" },
85525
85666
  { label: "Create Project", command: "/project-create" },
85667
+ { label: "Add chat to project", command: "/chat-add-to-project" },
85668
+ { label: "Move chat to project", command: "/chat-move-to-project" },
85669
+ { label: "Create project from chats", command: "/chat-create-project" },
85670
+ { label: "Show active chats", command: "/active" },
85526
85671
  { label: "New chat in Project", command: "/project-chat" },
85527
85672
  { label: "Edit workflow step", command: "/workflow-edit" },
85528
85673
  { label: "Enable or disable workflow", command: "/workflow-toggle" },
@@ -85537,6 +85682,259 @@ function paletteActions(query) {
85537
85682
  return TUI_ACTIONS.filter((item) => `${item.label} ${item.command}`.toLowerCase().includes(needle));
85538
85683
  }
85539
85684
 
85685
+ // src/tuiChatSidebar.ts
85686
+ import { createHash as createHash22, randomUUID as randomUUID18 } from "crypto";
85687
+
85688
+ // ../ui/src/utils/chatActivity.ts
85689
+ function processingAncestorIds(chats, runningIds) {
85690
+ const byId = new Map(chats.map((chat) => [chat.chat_id, chat]));
85691
+ const result = /* @__PURE__ */ new Set();
85692
+ for (const id of runningIds) {
85693
+ const ancestry = [];
85694
+ const visited = /* @__PURE__ */ new Set();
85695
+ let chat = byId.get(id);
85696
+ while (chat && !visited.has(chat.chat_id)) {
85697
+ visited.add(chat.chat_id);
85698
+ ancestry.push(chat);
85699
+ chat = chat.parent_id ? byId.get(chat.parent_id) : void 0;
85700
+ }
85701
+ if (ancestry.some((chat2) => chat2.is_hidden || chat2.is_hidden_candidate)) continue;
85702
+ for (const ancestor of ancestry) result.add(ancestor.chat_id);
85703
+ }
85704
+ return result;
85705
+ }
85706
+ function aggregateRunningChats(chats, runningIds) {
85707
+ const byId = new Map(chats.map((chat) => [chat.chat_id, chat]));
85708
+ const groups = /* @__PURE__ */ new Map();
85709
+ for (const id of runningIds) {
85710
+ let root = byId.get(id);
85711
+ if (!root) continue;
85712
+ const visited = /* @__PURE__ */ new Set([id]);
85713
+ let hidden = root.is_hidden || root.is_hidden_candidate;
85714
+ while (root.parent_id && byId.has(root.parent_id) && !visited.has(root.parent_id)) {
85715
+ visited.add(root.parent_id);
85716
+ root = byId.get(root.parent_id);
85717
+ hidden ||= root.is_hidden || root.is_hidden_candidate;
85718
+ }
85719
+ if (hidden) continue;
85720
+ let group = groups.get(root.chat_id);
85721
+ if (!group) {
85722
+ group = { chat: root, activeSubChatCount: 0 };
85723
+ groups.set(root.chat_id, group);
85724
+ }
85725
+ if (id !== root.chat_id) group.activeSubChatCount += 1;
85726
+ }
85727
+ return [...groups.values()];
85728
+ }
85729
+
85730
+ // ../ui/src/utils/chatProjectNavigation.ts
85731
+ function projectBreadcrumbs(project, folderId) {
85732
+ const ancestors = [];
85733
+ const visited = /* @__PURE__ */ new Set();
85734
+ let folder = project.folders.find((folder2) => folder2.id === folderId);
85735
+ while (folder && !visited.has(folder.id)) {
85736
+ visited.add(folder.id);
85737
+ ancestors.unshift({ id: folder.id, name: folder.name });
85738
+ folder = project.folders.find((candidate) => candidate.hash === folder.parentHash);
85739
+ }
85740
+ return [{ id: null, name: project.name }, ...ancestors];
85741
+ }
85742
+ function projectFolderChatIds(project, folderId, recursive = false) {
85743
+ const folder = project.folders.find((folder2) => folder2.id === folderId);
85744
+ if (folderId !== null && !folder) return /* @__PURE__ */ new Set();
85745
+ const hashes = /* @__PURE__ */ new Set([folder?.hash ?? null]);
85746
+ if (recursive) {
85747
+ let previous = -1;
85748
+ while (previous !== hashes.size) {
85749
+ previous = hashes.size;
85750
+ for (const candidate of project.folders) if (hashes.has(candidate.parentHash)) hashes.add(candidate.hash);
85751
+ }
85752
+ }
85753
+ return new Set(project.chats.filter((chat) => hashes.has(chat.folderHash)).map((chat) => chat.chatId));
85754
+ }
85755
+ function locationHasRunningChats(project, folderId, runningIds) {
85756
+ return [...projectFolderChatIds(project, folderId, true)].some((id) => runningIds.has(id));
85757
+ }
85758
+
85759
+ // src/tuiChatSidebar.ts
85760
+ function rowKey(row) {
85761
+ return [row.kind, row.chatId ?? "", row.projectId ?? "", row.folderId ?? ""].join(":");
85762
+ }
85763
+ function updateTuiChatSidebar(state, update) {
85764
+ const selected = tuiChatSidebarRows(state)[state.sidebarIndex];
85765
+ update();
85766
+ if (state.workspace !== "chats" || state.focus !== "sidebar") return;
85767
+ const rows2 = tuiChatSidebarRows(state);
85768
+ const index = selected ? rows2.findIndex((row) => rowKey(row) === rowKey(selected)) : -1;
85769
+ state.sidebarIndex = index >= 0 ? index : Math.max(0, Math.min(state.sidebarIndex, rows2.length - 1));
85770
+ }
85771
+ function runningTuiChatGroups(state) {
85772
+ const chats = new Map([...state.recentChats, ...state.activityChats].map((chat) => [chat.id, chat]));
85773
+ return aggregateRunningChats([...chats.values()].map((chat) => ({ chat_id: chat.id, parent_id: chat.parentId, is_hidden_candidate: chat.isHiddenCandidate })), new Set(state.runningChatIds)).map((group) => ({ ...group, chat: chats.get(group.chat.chat_id) }));
85774
+ }
85775
+ function tuiSidebarProjects(state) {
85776
+ return state.chatSidebarProjects.map((project) => ({
85777
+ id: project.id,
85778
+ name: project.name,
85779
+ folders: project.folders.map((folder) => ({
85780
+ id: folder.id,
85781
+ name: folder.name,
85782
+ hash: createHash22("sha256").update(folder.id).digest("hex"),
85783
+ parentHash: folder.parentHash ?? null
85784
+ })),
85785
+ chats: project.items.filter((item) => item.type === "chat").map((item) => ({
85786
+ id: item.id,
85787
+ chatId: item.targetId,
85788
+ folderHash: item.folderId ? createHash22("sha256").update(item.folderId).digest("hex") : null
85789
+ }))
85790
+ }));
85791
+ }
85792
+ function tuiChatBreadcrumb(state, width = 23) {
85793
+ const location = state.chatSidebarLocation;
85794
+ const project = tuiSidebarProjects(state).find((project2) => project2.id === location?.projectId);
85795
+ if (!project || !location) return null;
85796
+ const crumbs = projectBreadcrumbs(project, location.folderId);
85797
+ if (crumbs.length === 1) return truncateCells(project.name, width);
85798
+ const separator = crumbs.length > 2 ? " \u203A \u2026 \u203A " : " \u203A ";
85799
+ const current = crumbs.at(-1).name, budget = Math.max(2, width - cells(separator));
85800
+ if (cells(project.name) + cells(current) <= budget) return `${project.name}${separator}${current}`;
85801
+ const leafWidth = Math.min(cells(current), Math.max(1, budget - Math.min(cells(project.name), Math.ceil(budget / 2))));
85802
+ return `${truncateCells(project.name, budget - leafWidth)}${separator}${truncateCells(current, leafWidth)}`;
85803
+ }
85804
+ function tuiChatSidebarRows(state) {
85805
+ const groups = runningTuiChatGroups(state);
85806
+ const records = [...state.recentChats, ...state.activityChats].map((chat) => ({ chat_id: chat.id, parent_id: chat.parentId, is_hidden_candidate: chat.isHiddenCandidate }));
85807
+ const running = processingAncestorIds(records, new Set(state.runningChatIds));
85808
+ const rows2 = groups.map((group) => ({
85809
+ kind: "chat",
85810
+ chatId: group.chat.id,
85811
+ running: true,
85812
+ label: `${group.chat.title || "Untitled chat"}${group.activeSubChatCount ? ` (${group.activeSubChatCount} ${group.activeSubChatCount === 1 ? "subchat" : "subchats"})` : ""}`
85813
+ }));
85814
+ rows2.push({ kind: "new", label: "+ New chat" });
85815
+ if (state.chatProjectOperation) rows2.push({ kind: "create", label: "+ Create new project" });
85816
+ const projects = tuiSidebarProjects(state), location = state.chatSidebarLocation;
85817
+ const project = projects.find((project2) => project2.id === location?.projectId);
85818
+ let contents = null;
85819
+ if (project && location) {
85820
+ const crumbs = projectBreadcrumbs(project, location.folderId);
85821
+ rows2.push({ kind: "up", label: "\u2039 Up one level", projectId: project.id, folderId: crumbs.length > 1 ? crumbs[crumbs.length - 2].id : void 0 });
85822
+ if (crumbs.length > 2) rows2.push({ kind: "path", label: "\u2026 Show full path" });
85823
+ if (state.chatSidebarAncestors) for (const crumb of crumbs) rows2.push({ kind: "folder", label: crumb.name, projectId: project.id, folderId: crumb.id });
85824
+ if (state.chatProjectOperation) rows2.push({ kind: "choose", label: state.chatProjectOperation.mode === "move" ? "Move here" : "Add here", projectId: project.id, folderId: location.folderId });
85825
+ const hash2 = project.folders.find((folder) => folder.id === location.folderId)?.hash ?? null;
85826
+ rows2.push(...project.folders.filter((folder) => folder.parentHash === hash2).map((folder) => ({
85827
+ kind: "folder",
85828
+ label: folder.name,
85829
+ projectId: project.id,
85830
+ folderId: folder.id,
85831
+ running: locationHasRunningChats(project, folder.id, running)
85832
+ })));
85833
+ contents = projectFolderChatIds(project, location.folderId);
85834
+ } else rows2.push(...projects.map((project2) => ({
85835
+ kind: "project",
85836
+ label: project2.name,
85837
+ projectId: project2.id,
85838
+ running: locationHasRunningChats(project2, null, running)
85839
+ })));
85840
+ const organized = new Set(projects.flatMap((project2) => project2.chats.map((chat) => chat.chatId)));
85841
+ const candidates = new Map([...state.recentChats, ...state.sidebarLinkedChats].filter((chat) => !chat.isHiddenCandidate).map((chat) => [chat.id, { id: chat.id, title: chat.title, parentId: chat.parentId, isSubChat: chat.isSubChat }]));
85842
+ if (contents && location) for (const item of state.chatSidebarProjects.find((project2) => project2.id === location.projectId)?.items ?? []) {
85843
+ const verified = candidates.get(item.targetId);
85844
+ if (item.type === "chat" && contents.has(item.targetId) && verified && !verified.title)
85845
+ candidates.set(item.targetId, { ...verified, title: item.name });
85846
+ }
85847
+ if (!state.chatProjectOperation) rows2.push(...[...candidates.values()].filter((chat) => !running.has(chat.id) && (contents ? contents.has(chat.id) : !chat.parentId && !chat.isSubChat && !organized.has(chat.id))).map((chat) => ({ kind: "chat", chatId: chat.id, label: chat.title || "Untitled chat" })));
85848
+ return rows2;
85849
+ }
85850
+ async function refreshTuiChatSidebar(state, client2, render, includeProjects = false) {
85851
+ if (!state.signedIn) return;
85852
+ const team = typeof client2.getActiveTeamId === "function" ? client2.getActiveTeamId() : null;
85853
+ const master = typeof client2.getMasterKeyBytes === "function" ? Buffer.from(client2.getMasterKeyBytes()) : null;
85854
+ const current = () => state.signedIn && (typeof client2.getActiveTeamId !== "function" || client2.getActiveTeamId() === team) && (!master || master.equals(Buffer.from(client2.getMasterKeyBytes())));
85855
+ if (typeof client2.getChatActivity === "function") {
85856
+ try {
85857
+ const activity = await client2.getChatActivity();
85858
+ if (current()) {
85859
+ updateTuiChatSidebar(state, () => {
85860
+ state.runningChatIds = activity.ids;
85861
+ state.activityChats = activity.chats;
85862
+ });
85863
+ render();
85864
+ }
85865
+ } catch {
85866
+ if (current()) {
85867
+ state.status = "Running chat status unavailable.";
85868
+ render();
85869
+ }
85870
+ }
85871
+ }
85872
+ if (includeProjects && typeof client2.listProjects === "function") {
85873
+ const summaries = await loadTuiProjects(client2), projects = [];
85874
+ for (let start = 0; start < summaries.length; start += 4) projects.push(...await Promise.all(summaries.slice(start, start + 4).map((project) => loadTuiProject(client2, project.id, true))));
85875
+ const linkedIds = [...new Set(projects.flatMap((project) => project.items.filter((item) => item.type === "chat").map((item) => item.targetId)))];
85876
+ const linkedChats = typeof client2.getSidebarChats === "function" ? await client2.getSidebarChats(linkedIds) : [];
85877
+ if (current()) {
85878
+ updateTuiChatSidebar(state, () => {
85879
+ state.chatSidebarProjects = projects;
85880
+ state.sidebarLinkedChats = linkedChats;
85881
+ });
85882
+ render();
85883
+ }
85884
+ }
85885
+ }
85886
+ async function placeTuiChats(state, client2, chatIds, location, mode) {
85887
+ const context = { teamId: client2.getActiveTeamId() }, master = Buffer.from(client2.getMasterKeyBytes());
85888
+ const fence = () => {
85889
+ if (!state.signedIn || context.teamId !== client2.getActiveTeamId() || !master.equals(Buffer.from(client2.getMasterKeyBytes()))) throw new Error("Chat workspace changed");
85890
+ };
85891
+ const destination = await loadTuiProject(client2, location.projectId, true);
85892
+ if (location.folderId && !destination.folders.some((folder) => folder.id === location.folderId)) throw new Error("Folder unavailable");
85893
+ for (const chatId of chatIds) {
85894
+ fence();
85895
+ const existing = destination.items.find((item) => item.type === "chat" && item.targetId === chatId);
85896
+ if (existing) {
85897
+ if ((existing.folderId ?? null) !== location.folderId) await client2.moveProjectItemToFolder(destination.id, existing.id, location.folderId, context);
85898
+ } else {
85899
+ const title = [...state.recentChats, ...state.activityChats].find((chat) => chat.id === chatId)?.title || "Untitled chat";
85900
+ const timestamp = Math.floor(Date.now() / 1e3);
85901
+ const payload = {
85902
+ project_item_id: randomUUID18(),
85903
+ folder_id: location.folderId,
85904
+ item_type: "chat",
85905
+ target_id: chatId,
85906
+ target_id_encrypted: await encryptWithAesGcmCombined(chatId, destination.projectKey),
85907
+ encrypted_display_name: await encryptWithAesGcmCombined(title, destination.projectKey),
85908
+ created_at: timestamp,
85909
+ updated_at: timestamp,
85910
+ position: timestamp
85911
+ };
85912
+ fence();
85913
+ await client2.createProjectItem(destination.id, payload, context);
85914
+ }
85915
+ }
85916
+ if (mode === "move") for (const source of state.chatSidebarProjects.filter((project) => project.id !== destination.id)) {
85917
+ for (const chatId of chatIds.filter((id) => source.items.some((item) => item.type === "chat" && item.targetId === id))) {
85918
+ fence();
85919
+ await client2.deleteProjectItemByTarget(source.id, "chat", chatId, context);
85920
+ }
85921
+ }
85922
+ }
85923
+ async function createTuiChatProject(state, client2, chatIds) {
85924
+ if (!state.signedIn || !chatIds.length) throw new Error("Choose a saved chat first.");
85925
+ const context = client2.getActiveTeamId(), master = Buffer.from(client2.getMasterKeyBytes());
85926
+ const titles = chatIds.slice(0, 8).map((id) => [...state.recentChats, ...state.activityChats, ...state.sidebarLinkedChats].find((chat) => chat.id === id)?.title?.slice(0, 200) || "Untitled chat");
85927
+ const plan = await client2.planProjectAsk({ instruction: "Name a new project from chat titles.", chatTitles: titles });
85928
+ const proposed = plan.proposed_project, name = proposed?.name?.trim();
85929
+ if (!name || name.length > 200) throw new Error("Could not generate a project title.");
85930
+ if (context !== client2.getActiveTeamId() || !master.equals(Buffer.from(client2.getMasterKeyBytes()))) throw new Error("Chat workspace changed");
85931
+ const form = buildProjectForm();
85932
+ form.fields.find((field) => field.name === "name").value = name;
85933
+ const project = await submitProjectForm(client2, form, true);
85934
+ await placeTuiChats(state, client2, chatIds, { projectId: project.id, folderId: null }, "move");
85935
+ return project.id;
85936
+ }
85937
+
85540
85938
  // src/tuiLayout.ts
85541
85939
  var WORKSPACES = ["chats", "apps", "projects", "workflows", "tasks"];
85542
85940
  var DEFAULT_CHAT_BACKGROUND = "#4867cd";
@@ -85547,7 +85945,13 @@ function sidebarLines(state) {
85547
85945
  const selected = (label3, index) => `${state.focus === "sidebar" && state.sidebarIndex === index ? ">" : " "} ${label3}`;
85548
85946
  switch (state.workspace) {
85549
85947
  case "chats":
85550
- return ["Chats", "", selected("+ New chat", 0), "", "Recent chats", "", ...state.recentChats.map((c, i) => selected(c.title || "Untitled chat", i + 1)), "", "/chats browse /examples"];
85948
+ return [
85949
+ tuiChatBreadcrumb(state) ?? "Chats",
85950
+ "",
85951
+ ...tuiChatSidebarRows(state).map((row, index) => selected(`${row.running ? ["\u25F4", "\u25F7", "\u25F6", "\u25F5"][state.activityFrame % 4] + " " : ""}${row.label}`, index)),
85952
+ "",
85953
+ "/active /chat-add-to-project"
85954
+ ];
85551
85955
  case "projects":
85552
85956
  return ["Projects", "", ...state.projects.map((p, i) => selected(p.name, i)), "", "/project-create new"];
85553
85957
  case "tasks":
@@ -85572,7 +85976,7 @@ function workspaceHint(state) {
85572
85976
  if (state.screen === "app") return "1\u20135 tabs \u2191/\u2193 choose Enter open Esc Apps /search";
85573
85977
  if (state.screen === "app-skill") return "1\u20133 tabs Enter use skill Esc app Ctrl+P actions";
85574
85978
  if (state.screen === "app-result") return "\u2191/\u2193 scroll Esc saved results Ctrl+P actions";
85575
- if (state.screen === "apps") return "\u2191/\u2193 choose Enter open /search /browse Show all Tab focus";
85979
+ if (state.screen === "apps") return state.focus === "composer" ? "Type /command Shift+Tab apps Ctrl+B sidebar" : "\u2190/\u2192 app Enter open \u2191/\u2193 scroll /search /browse Show all Ctrl+B sidebar";
85576
85980
  if (state.screen === "projects" || state.screen === "tasks" || state.screen === "workflows") return "\u2191/\u2193 choose Enter open /search filter Tab focus Ctrl+P actions";
85577
85981
  if (state.screen === "interests") return "\u2191/\u2193 move Space select Enter continue Esc back";
85578
85982
  if (state.screen === "examples") return "\u2191/\u2193 choose Enter open /search filter Esc back";
@@ -85637,7 +86041,7 @@ function renderWorkspaceFrame(state, rawWidth, rawHeight, body, options = {}) {
85637
86041
  const markers = content.map((row, index) => /(?:^|[│|])\s*[>›] /.test(lineText(row)) ? index : -1).filter((index) => index >= 0);
85638
86042
  const marker = state.screen === "workflow" ? markers.at(-1) ?? -1 : markers[0] ?? -1;
85639
86043
  let selectionEnd = marker;
85640
- if (["workflow", "start", "chats"].includes(state.screen) && marker >= 0) {
86044
+ if (["workflow", "start", "chats", "apps"].includes(state.screen) && marker >= 0) {
85641
86045
  const closing = content.findIndex((row, index) => index > marker && lineText(row).includes("\u2570"));
85642
86046
  selectionEnd = Math.min(marker + 10, closing < 0 ? marker : closing);
85643
86047
  }
@@ -85690,6 +86094,48 @@ function titleCase(value) {
85690
86094
  return value.charAt(0).toUpperCase() + value.slice(1);
85691
86095
  }
85692
86096
 
86097
+ // src/tuiCarousel.ts
86098
+ function renderCardCarousel(cards, width, selectedIndex, focused) {
86099
+ width = Math.max(1, Math.floor(width));
86100
+ if (!cards.length) return [];
86101
+ const selected = Math.max(0, Math.min(cards.length - 1, selectedIndex));
86102
+ const cardWidth = Math.min(width, 36), inner = Math.max(1, cardWidth - 4), stride = cardWidth + 2;
86103
+ const center = Math.floor((width - cardWidth) / 2);
86104
+ const reach = Math.ceil(width / stride);
86105
+ const visible = cards.slice(Math.max(0, selected - reach), selected + reach + 1).map((card, offset) => {
86106
+ const index = Math.max(0, selected - reach) + offset, active = index === selected && focused;
86107
+ const title = wrapCells(`${active ? "\u203A " : ""}${card.title}`, inner).slice(0, 2);
86108
+ const description = wrapCells(card.description, inner).slice(0, 2);
86109
+ while (title.length < 2) title.push("");
86110
+ while (description.length < 2) description.push("");
86111
+ const rows2 = cardWidth < 4 ? [truncateCells(card.title, cardWidth)] : [
86112
+ `\u256D${"\u2500".repeat(cardWidth - 2)}\u256E`,
86113
+ ...title.map((line) => `\u2502 ${padCells(line, inner)} \u2502`),
86114
+ ...description.map((line) => `\u2502 ${padCells(line, inner)} \u2502`),
86115
+ `\u2502 ${padCells(card.footer, inner)} \u2502`,
86116
+ `\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`
86117
+ ];
86118
+ return { rows: rows2, left: center + (index - selected) * stride, background: card.background, active };
86119
+ });
86120
+ return Array.from({ length: visible[0].rows.length }, (_, row) => {
86121
+ const spans = [];
86122
+ let column = 0;
86123
+ for (const card of visible) {
86124
+ const start = Math.max(0, card.left), end = Math.min(width, card.left + cardWidth);
86125
+ if (end <= start) continue;
86126
+ if (start > column) spans.push({ text: " ".repeat(start - column) });
86127
+ spans.push({ text: sliceCells(card.rows[row], Math.max(0, -card.left), end - start), background: card.background, bold: card.active });
86128
+ column = end;
86129
+ }
86130
+ if (column < width) spans.push({ text: " ".repeat(width - column) });
86131
+ return { text: spans.map((span) => span.text).join(""), spans };
86132
+ });
86133
+ }
86134
+ function centeredCarouselText(text4, width) {
86135
+ const value = truncateCells(text4, width);
86136
+ return " ".repeat(Math.max(0, Math.floor((width - cells(value)) / 2))) + value;
86137
+ }
86138
+
85693
86139
  // ../workspaceInspirationDefaults.ts
85694
86140
  function getWorkspaceInspirations(surface) {
85695
86141
  const now = Math.floor(Date.now() / 1e3);
@@ -85950,10 +86396,7 @@ async function loadHomeData(state, client2, render) {
85950
86396
  render();
85951
86397
  }
85952
86398
  }
85953
- function centered2(text4, width) {
85954
- const value = truncateCells(text4, width);
85955
- return " ".repeat(Math.max(0, Math.floor((width - cells(value)) / 2))) + value;
85956
- }
86399
+ var centered2 = centeredCarouselText;
85957
86400
  function homeHeader(state, width, height) {
85958
86401
  const inspiration = currentInspiration(state), rows2 = Math.max(4, Math.min(8, Math.floor(height / 5))), result = [];
85959
86402
  const gradient = inspiration?.category && CATEGORY_GRADIENTS[inspiration.category] || CATEGORY_GRADIENTS.general_knowledge;
@@ -85971,46 +86414,20 @@ function homeHeader(state, width, height) {
85971
86414
  }
85972
86415
  function renderHomeChatCards(state, width, height) {
85973
86416
  const chats = homeChatItems(state), result = homeHeader(state, width, height);
86417
+ const activeCount = runningTuiChatGroups(state).length;
86418
+ if (activeCount) result.push(centered2(`${activeCount} ${activeCount === 1 ? "chat" : "chats"} active\u2026 /active`, width), "");
85974
86419
  if (!chats.length) {
85975
86420
  result.push(centered2(state.homeLoading ? "Loading your recent chats\u2026" : "Start a chat below. Your recent chats will appear here.", width), "");
85976
86421
  return result;
85977
86422
  }
85978
86423
  result.push(centered2(state.signedIn ? "Continue where you left off" : "Explore example chats", width), "");
85979
86424
  const selected = Math.max(0, Math.min(chats.length - 1, state.selectedIndex));
85980
- const cardWidth = Math.min(width, 36), inner = Math.max(1, cardWidth - 4), stride = cardWidth + 2;
85981
- const slots = Math.max(1, Math.floor((width + 2) / stride));
85982
- const first = Math.max(0, selected - slots + 1);
85983
- const cards = chats.slice(first, first + slots + 1).map((chat, index) => {
85984
- const active = first + index === selected && state.focus === "content";
85985
- const title = wrapCells(`${active ? "\u203A " : ""}${chat.title || "Untitled chat"}`, inner).slice(0, 2);
85986
- const summary = wrapCells(chat.summary || "Continue this conversation", inner).slice(0, 2);
85987
- while (title.length < 2) title.push("");
85988
- while (summary.length < 2) summary.push("");
85989
- const rows2 = cardWidth < 4 ? [truncateCells(chat.title || "Untitled chat", cardWidth)] : [
85990
- `\u256D${"\u2500".repeat(cardWidth - 2)}\u256E`,
85991
- ...title.map((line) => `\u2502 ${padCells(line, inner)} \u2502`),
85992
- ...summary.map((line) => `\u2502 ${padCells(line, inner)} \u2502`),
85993
- `\u2502 ${padCells(chat.category?.replaceAll("_", " ") || "Chat", inner)} \u2502`,
85994
- `\u2570${"\u2500".repeat(cardWidth - 2)}\u256F`
85995
- ];
85996
- return { rows: rows2, background: (chat.category && CATEGORY_GRADIENTS[chat.category] || BLUE).start, active };
85997
- });
85998
- for (let row = 0; row < cards[0].rows.length; row++) {
85999
- const spans = [];
86000
- let remaining = width;
86001
- for (const card of cards) {
86002
- if (!remaining) break;
86003
- const text4 = truncateCells(card.rows[row], remaining);
86004
- spans.push({ text: text4, background: card.background, bold: card.active });
86005
- remaining -= cells(text4);
86006
- if (remaining) {
86007
- const gap = " ".repeat(Math.min(2, remaining));
86008
- spans.push({ text: gap });
86009
- remaining -= gap.length;
86010
- }
86011
- }
86012
- result.push({ text: spans.map((span) => span.text).join(""), spans });
86013
- }
86425
+ result.push(...renderCardCarousel(chats.map((chat) => ({
86426
+ title: chat.title || "Untitled chat",
86427
+ description: chat.summary || "Continue this conversation",
86428
+ footer: chat.category?.replaceAll("_", " ") || "Chat",
86429
+ background: (chat.category && CATEGORY_GRADIENTS[chat.category] || BLUE).start
86430
+ })), width, selected, state.focus === "content"));
86014
86431
  result.push(
86015
86432
  "",
86016
86433
  centered2(`${selected > 0 ? "\u2039" : " "} Chat ${selected + 1} of ${chats.length} ${selected < chats.length - 1 ? "\u203A" : " "}`, width),
@@ -86022,7 +86439,7 @@ function renderHomeChatCards(state, width, height) {
86022
86439
  }
86023
86440
 
86024
86441
  // src/tuiAppsWorkspace.ts
86025
- import { randomBytes as randomBytes11, randomUUID as randomUUID18 } from "crypto";
86442
+ import { randomBytes as randomBytes11, randomUUID as randomUUID19 } from "crypto";
86026
86443
 
86027
86444
  // ../ui/src/components/apps/appsSkillFormUtils.ts
86028
86445
  var object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -86228,20 +86645,26 @@ function visibleTuiApps(apps, query = "") {
86228
86645
  const words = query.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
86229
86646
  return apps.filter((app) => words.every((word) => `${app.name} ${app.description} ${app.category} ${app.skills.map((skill) => skill.name).join(" ")}`.toLocaleLowerCase().includes(word)));
86230
86647
  }
86648
+ function homeTuiApps(apps, query = "", showAll = false) {
86649
+ const visible = visibleTuiApps(apps, query);
86650
+ return showAll || query ? visible : visible.slice(0, HOME_APP_ORDER.length);
86651
+ }
86231
86652
  function renderTuiAppsHome(apps, options) {
86232
86653
  const width = Math.max(1, options.width);
86233
86654
  const visible = visibleTuiApps(apps, options.query);
86234
- const lines = [];
86235
- if (!visible.length) lines.push(options.query ? "No matching apps." : "No apps available.");
86236
- for (const app of visible) {
86237
- lines.push(...cardLines(
86238
- `${app.id === options.selectedId ? "\u203A" : " "} ${app.name}`,
86239
- app.description,
86240
- `${app.skills.length} skills${app.category ? ` \xB7 ${app.category.replaceAll("_", " ")}` : ""}`,
86241
- width
86242
- ), "");
86243
- }
86244
- return lines;
86655
+ if (!visible.length) return [centeredCarouselText(options.query ? "No matching apps." : "No apps available.", width)];
86656
+ const selected = Math.max(0, Math.min(visible.length - 1, options.selectedIndex ?? visible.findIndex((app) => app.id === options.selectedId)));
86657
+ return [
86658
+ ...renderCardCarousel(visible.map((app) => ({
86659
+ title: app.name,
86660
+ description: app.description,
86661
+ footer: `${app.skills.length} skills${app.category ? ` \xB7 ${app.category.replaceAll("_", " ")}` : ""}`,
86662
+ background: (APP_GRADIENTS[app.id] ?? PRIMARY_GRADIENT).start
86663
+ })), width, selected, options.selectedId !== void 0),
86664
+ "",
86665
+ centeredCarouselText(`${selected > 0 ? "\u2039" : " "} App ${selected + 1} of ${visible.length} ${selected < visible.length - 1 ? "\u203A" : " "}`, width),
86666
+ centeredCarouselText("\u2190/\u2192 choose app \xB7 Enter open \xB7 Tab focus", width)
86667
+ ];
86245
86668
  }
86246
86669
  function cardLines(title, description, footer, width) {
86247
86670
  if (width < 4) return [truncateCells(title, width)];
@@ -86473,7 +86896,7 @@ async function saveEncryptedResult(client2, ownerId, rootId, key, encryptedKey,
86473
86896
  const value = record5(result);
86474
86897
  const type = string2(value.type, string2(value.embed_type));
86475
86898
  if (!type && typeof value.embed_id !== "string") return [];
86476
- const id = typeof value.embed_id === "string" && UUID.test(value.embed_id) ? value.embed_id : randomUUID18();
86899
+ const id = typeof value.embed_id === "string" && UUID.test(value.embed_id) ? value.embed_id : randomUUID19();
86477
86900
  return [(async () => ({
86478
86901
  embed_id: id,
86479
86902
  encrypted_type: await encryptWithAesGcmCombined(type || "app_skill_use", key),
@@ -86518,7 +86941,7 @@ async function executeConfirmedTuiAppsSkill(client2, run) {
86518
86941
  const owner = record5(await client2.whoAmI());
86519
86942
  const ownerId = string2(owner.id, string2(owner.user_id));
86520
86943
  if (!ownerId) throw new Error("Account identity unavailable; skill was not run.");
86521
- const rootId = randomUUID18();
86944
+ const rootId = randomUUID19();
86522
86945
  const key = randomBytes11(32);
86523
86946
  const encryptedKey = await encryptBytesWithAesGcm(key, client2.getMasterKeyBytes());
86524
86947
  await saveEncryptedResult(client2, ownerId, rootId, key, encryptedKey, run, { status: "processing" }, "processing");
@@ -86764,6 +87187,15 @@ function createInitialTuiState() {
86764
87187
  taskStatusFilter: "",
86765
87188
  taskContext: null,
86766
87189
  recentChats: [],
87190
+ runningChatIds: [],
87191
+ activityChats: [],
87192
+ sidebarLinkedChats: [],
87193
+ activityFrame: 0,
87194
+ chatSidebarProjects: [],
87195
+ chatSidebarLocation: null,
87196
+ chatSidebarAncestors: false,
87197
+ chatProjectOperation: null,
87198
+ chatProjectBusy: false,
86767
87199
  activeChatId: null,
86768
87200
  activeChat: null,
86769
87201
  headerState: "new",
@@ -86888,8 +87320,13 @@ function renderBody(state, width, height) {
86888
87320
  case "chats":
86889
87321
  return renderHomeChatCards(state, width, height);
86890
87322
  case "apps": {
86891
- const apps = visibleTuiApps(state.apps, state.filter), visible = state.homeShowAll || state.filter ? apps : apps.slice(0, Math.max(6, state.selectedIndex + 1));
86892
- return [...homeHeader(state, width, height), ...homeCards(renderTuiAppsHome(visible, { width: Math.min(width, 88), selectedId: state.focus === "content" ? apps[state.selectedIndex]?.id : void 0 }), width, visible.map((app) => APP_GRADIENTS[app.id] ?? PRIMARY_GRADIENT)), "Show all \xB7 /search Search apps"];
87323
+ const apps = homeTuiApps(state.apps, state.filter, state.homeShowAll);
87324
+ return [
87325
+ ...homeHeader(state, width, height),
87326
+ ...renderTuiAppsHome(apps, { width, selectedIndex: state.selectedIndex, selectedId: state.focus === "content" ? apps[state.selectedIndex]?.id : void 0 }),
87327
+ "",
87328
+ centeredCarouselText(`${state.homeShowAll ? "/browse Featured apps" : "/browse Show all"} \xB7 /search Search apps`, width)
87329
+ ];
86893
87330
  }
86894
87331
  case "app": {
86895
87332
  const app = state.activeApp;
@@ -87170,7 +87607,7 @@ function wrap(line, width) {
87170
87607
  }
87171
87608
 
87172
87609
  // src/tuiWorkspaceController.ts
87173
- import { randomUUID as randomUUID19 } from "crypto";
87610
+ import { randomUUID as randomUUID20 } from "crypto";
87174
87611
  function chatDraftKey(state) {
87175
87612
  return state.screen === "example" ? `example:${state.activeExample?.chat.id}` : state.activeChatId ?? "new";
87176
87613
  }
@@ -87217,6 +87654,7 @@ async function recent(context) {
87217
87654
  if (request !== state.routeVersion || homeRequest !== state.homeLoadVersion || !state.signedIn) return;
87218
87655
  state.recentChats = chats;
87219
87656
  render();
87657
+ await refreshTuiChatSidebar(state, client2, render, true);
87220
87658
  }
87221
87659
  async function openSavedChat(context, id) {
87222
87660
  const { state, client: client2, render } = context;
@@ -87418,6 +87856,64 @@ async function handleWorkspaceCommand(context, command) {
87418
87856
  state.selectedProjectId = null;
87419
87857
  render();
87420
87858
  return true;
87859
+ case "/active":
87860
+ state.sidebarOpen = true;
87861
+ state.focus = "sidebar";
87862
+ state.sidebarIndex = 0;
87863
+ render();
87864
+ await refreshTuiChatSidebar(state, client2, render, true);
87865
+ state.sidebarIndex = 0;
87866
+ render();
87867
+ return true;
87868
+ case "/chat-add-to-project":
87869
+ case "/chat-move-to-project":
87870
+ case "/chat-create-project": {
87871
+ if (state.chatProjectBusy) return true;
87872
+ if (!state.signedIn) throw new Error("Sign in to organize chats.");
87873
+ const selected = state.focus === "sidebar" ? tuiChatSidebarRows(state)[state.sidebarIndex]?.chatId : ["start", "chats"].includes(state.screen) ? homeChatItems(state)[state.selectedIndex]?.id : state.activeChatId;
87874
+ const ids = arg ? arg.split(/\s+/).filter(Boolean) : selected ? [selected] : [];
87875
+ if (!ids.length) throw new Error("Choose a saved chat first.");
87876
+ await refreshTuiChatSidebar(state, client2, render, true);
87877
+ if (name === "/chat-create-project") {
87878
+ state.chatProjectBusy = true;
87879
+ state.status = "Organizing chats\u2026";
87880
+ render();
87881
+ try {
87882
+ const id = await createTuiChatProject(state, client2, ids);
87883
+ state.chatProjectOperation = null;
87884
+ await openProject(context, id);
87885
+ } finally {
87886
+ state.chatProjectBusy = false;
87887
+ }
87888
+ } else {
87889
+ state.chatProjectOperation = { chatIds: ids, mode: name === "/chat-move-to-project" ? "move" : "add" };
87890
+ state.chatSidebarLocation = null;
87891
+ state.sidebarOpen = true;
87892
+ state.focus = "sidebar";
87893
+ state.sidebarIndex = 0;
87894
+ state.status = "Choose a project or subfolder.";
87895
+ }
87896
+ render();
87897
+ return true;
87898
+ }
87899
+ case "/chat-subfolder": {
87900
+ const location = state.chatSidebarLocation;
87901
+ if (!location || !arg.trim()) throw new Error("Open a project folder and enter /chat-subfolder followed by a name.");
87902
+ const project = state.chatSidebarProjects.find((project2) => project2.id === location.projectId);
87903
+ if (!project) throw new Error("Project unavailable.");
87904
+ const timestamp = Math.floor(Date.now() / 1e3);
87905
+ await client2.createProjectFolder(project.id, {
87906
+ folder_id: randomUUID20(),
87907
+ parent_folder_id: location.folderId,
87908
+ encrypted_name: await encryptWithAesGcmCombined(arg.trim().slice(0, 200), project.projectKey),
87909
+ encrypted_sort_key: await encryptWithAesGcmCombined(arg.trim().toLowerCase().slice(0, 200), project.projectKey),
87910
+ created_at: timestamp,
87911
+ updated_at: timestamp,
87912
+ position: timestamp
87913
+ });
87914
+ await refreshTuiChatSidebar(state, client2, render, true);
87915
+ return true;
87916
+ }
87421
87917
  case "/sidebar":
87422
87918
  state.sidebarOpen = !state.sidebarOpen;
87423
87919
  state.focus = state.sidebarOpen ? "sidebar" : state.workspace === "chats" && !["start", "chats"].includes(state.screen) ? "composer" : "content";
@@ -87658,7 +88154,7 @@ async function saveForm(context) {
87658
88154
  } else if (form.kind === "workflow-create") {
87659
88155
  const description = formValue(form, "description").trim();
87660
88156
  if (!description) throw new Error("Describe what this workflow should do.");
87661
- const request = state.routeVersion, session = await client2.startWorkflowInput({ text: description, inputType: "text", optimisticSave: true, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, idempotencyKey: randomUUID19() });
88157
+ const request = state.routeVersion, session = await client2.startWorkflowInput({ text: description, inputType: "text", optimisticSave: true, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, idempotencyKey: randomUUID20() });
87662
88158
  if (request !== state.routeVersion || state.form !== form) return;
87663
88159
  const workflow = session.workflow ?? session.workflows?.[0];
87664
88160
  if (workflow) {
@@ -87684,7 +88180,7 @@ async function saveForm(context) {
87684
88180
  }
87685
88181
  }
87686
88182
  async function handleWorkspaceKey(context, chunk, key) {
87687
- const { state, render } = context;
88183
+ const { state, render, client: client2 } = context;
87688
88184
  if (state.form) {
87689
88185
  const form = state.form, field = form.fields[form.fieldIndex];
87690
88186
  if (!field) {
@@ -87750,8 +88246,8 @@ async function handleWorkspaceKey(context, chunk, key) {
87750
88246
  return true;
87751
88247
  }
87752
88248
  if (state.workflowEdit) return false;
87753
- const chatHome = state.screen === "start" || state.screen === "chats";
87754
- if (["pageup", "pagedown", "home", "end", "scrollup", "scrolldown"].includes(key.name ?? "") || (chatHome || state.screen === "project" && state.projectTab === "overview") && ["up", "down"].includes(key.name ?? "") && ["composer", "content"].includes(state.focus) && !state.input.startsWith("/")) {
88249
+ const chatHome = state.screen === "start" || state.screen === "chats", carouselHome = chatHome || state.screen === "apps";
88250
+ if (["pageup", "pagedown", "home", "end", "scrollup", "scrolldown"].includes(key.name ?? "") || (carouselHome || state.screen === "project" && state.projectTab === "overview") && ["up", "down"].includes(key.name ?? "") && ["composer", "content"].includes(state.focus) && !state.input.startsWith("/")) {
87755
88251
  const bottom = state.screen === "chat";
87756
88252
  state.followSelection = false;
87757
88253
  if (key.name === "home") state.scrollOffset = bottom ? 1e4 : 0;
@@ -87765,6 +88261,13 @@ async function handleWorkspaceKey(context, chunk, key) {
87765
88261
  return true;
87766
88262
  }
87767
88263
  if (key.name === "escape") {
88264
+ if (state.chatProjectBusy) return true;
88265
+ if (state.chatProjectOperation) {
88266
+ state.chatProjectOperation = null;
88267
+ state.status = null;
88268
+ render();
88269
+ return true;
88270
+ }
87768
88271
  if (state.focus === "sidebar") {
87769
88272
  state.sidebarOpen = false;
87770
88273
  state.focus = chatHome ? "content" : state.workspace === "chats" ? "composer" : "content";
@@ -87824,8 +88327,9 @@ async function handleWorkspaceKey(context, chunk, key) {
87824
88327
  render();
87825
88328
  return true;
87826
88329
  }
87827
- if (chatHome && state.focus === "content" && (key.name === "left" || key.name === "right")) {
87828
- state.selectedIndex = Math.max(0, Math.min(Math.max(0, homeChatItems(state).length - 1), state.selectedIndex + (key.name === "left" ? -1 : 1)));
88330
+ if (carouselHome && state.focus === "content" && (key.name === "left" || key.name === "right")) {
88331
+ const count = chatHome ? homeChatItems(state).length : homeTuiApps(state.apps, state.filter, state.homeShowAll).length;
88332
+ state.selectedIndex = Math.max(0, Math.min(Math.max(0, count - 1), state.selectedIndex + (key.name === "left" ? -1 : 1)));
87829
88333
  state.followSelection = true;
87830
88334
  render();
87831
88335
  return true;
@@ -87840,11 +88344,34 @@ async function handleWorkspaceKey(context, chunk, key) {
87840
88344
  if (chunk !== "/") return true;
87841
88345
  }
87842
88346
  if (state.focus === "sidebar") {
87843
- const count = state.workspace === "chats" ? state.recentChats.length + 1 : state.workspace === "projects" ? state.projects.length : state.workspace === "tasks" ? state.tasks.length : state.workspace === "apps" ? state.apps.length : state.workflows.length;
88347
+ const chatRows = tuiChatSidebarRows(state);
88348
+ const count = state.workspace === "chats" ? chatRows.length : state.workspace === "projects" ? state.projects.length : state.workspace === "tasks" ? state.tasks.length : state.workspace === "apps" ? state.apps.length : state.workflows.length;
87844
88349
  if (key.name === "up" || key.name === "down") state.sidebarIndex = Math.max(0, Math.min(count - 1, state.sidebarIndex + (key.name === "up" ? -1 : 1)));
87845
88350
  else if (key.name === "return") {
87846
- if (state.workspace === "chats") await context.command(state.sidebarIndex === 0 ? "/new" : `/chat ${state.recentChats[state.sidebarIndex - 1]?.id}`);
87847
- else if (state.workspace === "projects") await context.command(`/project ${state.projects[state.sidebarIndex]?.id}`);
88351
+ if (state.workspace === "chats") {
88352
+ if (state.chatProjectBusy) return true;
88353
+ const row = chatRows[state.sidebarIndex];
88354
+ if (row?.kind === "new") await context.command("/new");
88355
+ else if (row?.kind === "chat") await context.command(`/chat ${row.chatId}`);
88356
+ else if (row?.kind === "path") state.chatSidebarAncestors = !state.chatSidebarAncestors;
88357
+ else if (row?.kind === "create" && state.chatProjectOperation) await context.command(`/chat-create-project ${state.chatProjectOperation.chatIds.join(" ")}`);
88358
+ else if (row?.kind === "choose" && state.chatProjectOperation) {
88359
+ const operation = state.chatProjectOperation;
88360
+ state.chatProjectBusy = true;
88361
+ try {
88362
+ await placeTuiChats(state, client2, operation.chatIds, { projectId: row.projectId, folderId: row.folderId ?? null }, operation.mode);
88363
+ state.chatProjectOperation = null;
88364
+ state.status = null;
88365
+ } finally {
88366
+ state.chatProjectBusy = false;
88367
+ await refreshTuiChatSidebar(state, client2, render, true);
88368
+ }
88369
+ } else if (row && ["project", "folder", "up"].includes(row.kind)) {
88370
+ state.chatSidebarLocation = row.kind === "up" && row.folderId === void 0 ? null : { projectId: row.projectId, folderId: row.folderId ?? null };
88371
+ state.chatSidebarAncestors = false;
88372
+ state.sidebarIndex = 0;
88373
+ }
88374
+ } else if (state.workspace === "projects") await context.command(`/project ${state.projects[state.sidebarIndex]?.id}`);
87848
88375
  else if (state.workspace === "tasks") await openTask(context, state.tasks[state.sidebarIndex]?.taskId ?? "");
87849
88376
  else if (state.workspace === "workflows") await context.command(`/workflow ${state.workflows[state.sidebarIndex]?.id}`);
87850
88377
  else if (state.workspace === "apps") await context.command(`/app ${state.apps[state.sidebarIndex]?.id}`);
@@ -87880,7 +88407,7 @@ async function handleWorkspaceKey(context, chunk, key) {
87880
88407
  }
87881
88408
  const resultsTab = state.screen === "app" ? state.appTab === "embeds" : state.screen === "app-skill" && state.appSkillTab === "embeds";
87882
88409
  const workflowsTab = state.screen === "app" ? state.appTab === "workflows" : state.screen === "app-skill" && state.appSkillTab === "workflows";
87883
- const count = state.screen === "apps" ? visibleTuiApps(state.apps, state.filter).length : resultsTab ? state.appResults.items.length : workflowsTab ? state.appWorkflows.items.length : state.activeApp?.skills.length ?? 0;
88410
+ const count = state.screen === "apps" ? homeTuiApps(state.apps, state.filter, state.homeShowAll).length : resultsTab ? state.appResults.items.length : workflowsTab ? state.appWorkflows.items.length : state.activeApp?.skills.length ?? 0;
87884
88411
  if (key.name === "up" || key.name === "down") {
87885
88412
  state.selectedIndex = Math.max(0, Math.min(count - 1, state.selectedIndex + (key.name === "up" ? -1 : 1)));
87886
88413
  render();
@@ -87888,7 +88415,7 @@ async function handleWorkspaceKey(context, chunk, key) {
87888
88415
  }
87889
88416
  if (key.name === "return") {
87890
88417
  if (state.screen === "apps") {
87891
- const app = visibleTuiApps(state.apps, state.filter)[state.selectedIndex];
88418
+ const app = homeTuiApps(state.apps, state.filter, state.homeShowAll)[state.selectedIndex];
87892
88419
  if (app) await context.command(`/app ${app.id}`);
87893
88420
  } else if (resultsTab) {
87894
88421
  const result = state.appResults.items[state.selectedIndex];
@@ -88231,6 +88758,27 @@ async function runTui(client2, terminal = new TuiTerminal()) {
88231
88758
  let resolveResult = null;
88232
88759
  let renderTimer = null;
88233
88760
  let closed = false;
88761
+ let stopActivity;
88762
+ let activityTimer;
88763
+ let refreshingActivity = false;
88764
+ const refreshActivity = async () => {
88765
+ if (closed || refreshingActivity || !state.signedIn) return;
88766
+ refreshingActivity = true;
88767
+ try {
88768
+ await refreshTuiChatSidebar(state, client2, render);
88769
+ } finally {
88770
+ refreshingActivity = false;
88771
+ }
88772
+ };
88773
+ const activityPoll = setInterval(() => {
88774
+ void refreshActivity();
88775
+ }, 3e4);
88776
+ const activityAnimation = setInterval(() => {
88777
+ if (!closed && state.runningChatIds.length) {
88778
+ state.activityFrame = (state.activityFrame + 1) % 4;
88779
+ render();
88780
+ }
88781
+ }, 250);
88234
88782
  const render = () => {
88235
88783
  if (closed) return;
88236
88784
  if (state.screen !== "chat") client2.clearInteractiveChatViewer();
@@ -88246,6 +88794,10 @@ async function runTui(client2, terminal = new TuiTerminal()) {
88246
88794
  };
88247
88795
  const finish = (result) => {
88248
88796
  closed = true;
88797
+ stopActivity?.();
88798
+ clearTimeout(activityTimer);
88799
+ clearInterval(activityPoll);
88800
+ clearInterval(activityAnimation);
88249
88801
  client2.endInteractiveViewerSession();
88250
88802
  if (renderTimer) clearTimeout(renderTimer);
88251
88803
  renderTimer = null;
@@ -88262,6 +88814,19 @@ async function runTui(client2, terminal = new TuiTerminal()) {
88262
88814
  });
88263
88815
  render();
88264
88816
  void loadHomeData(state, client2, render);
88817
+ void refreshActivity();
88818
+ if (state.signedIn && typeof client2.observeChatActivity === "function") {
88819
+ void client2.observeChatActivity(() => {
88820
+ clearTimeout(activityTimer);
88821
+ activityTimer = setTimeout(() => {
88822
+ void refreshActivity();
88823
+ }, 300);
88824
+ }).then((stop) => {
88825
+ if (closed) stop();
88826
+ else stopActivity = stop;
88827
+ }).catch(() => {
88828
+ });
88829
+ }
88265
88830
  return new Promise((resolve18) => {
88266
88831
  resolveResult = resolve18;
88267
88832
  });
@@ -88574,12 +89139,12 @@ async function sendTuiMessage(params) {
88574
89139
  if (sourceExample) state.messages = sourceExample.messages.map((m) => ({ role: m.role === "user" ? "user" : "assistant", content: m.content, title: m.senderName }));
88575
89140
  const messages = state.messages;
88576
89141
  const existingChatId = sourceExample ? null : state.activeChatId;
88577
- const chatId = existingChatId ?? randomUUID20();
89142
+ const chatId = existingChatId ?? randomUUID21();
88578
89143
  const draftKey = sourceExample ? `example:${sourceExample.chat.id}` : existingChatId ?? "new";
88579
89144
  state.drafts[draftKey] = "";
88580
89145
  state.input = "";
88581
89146
  state.inputCursor = null;
88582
- const anonymousHistory = history ?? messages.filter((m) => m.role !== "system").map((m) => ({ message_id: randomUUID20(), role: m.role, content: m.content, sender_name: m.title ?? (m.role === "user" ? "User" : "Assistant"), created_at: Math.floor(Date.now() / 1e3) }));
89147
+ const anonymousHistory = history ?? messages.filter((m) => m.role !== "system").map((m) => ({ message_id: randomUUID21(), role: m.role, content: m.content, sender_name: m.title ?? (m.role === "user" ? "User" : "Assistant"), created_at: Math.floor(Date.now() / 1e3) }));
88583
89148
  state.activeChatId = chatId;
88584
89149
  state.headerState = existingChatId ? "ready" : "loading";
88585
89150
  state.headerError = null;
@@ -89004,7 +89569,7 @@ function clamp(value, min, max) {
89004
89569
  }
89005
89570
 
89006
89571
  // src/codexTaskBridge.ts
89007
- import { createHash as createHash22 } from "crypto";
89572
+ import { createHash as createHash23 } from "crypto";
89008
89573
  import { spawn as spawn3 } from "child_process";
89009
89574
  import { existsSync as existsSync15, readFileSync as readFileSync16, readdirSync as readdirSync8 } from "fs";
89010
89575
  import { isAbsolute as isAbsolute4, join as join18, resolve as resolve10 } from "path";
@@ -89047,7 +89612,7 @@ async function flushCodexChatDeletions(client2, teamId, stateRoot, notice) {
89047
89612
  const session = client2.getSession();
89048
89613
  const scope2 = [session.apiUrl, session.hashedEmail, teamId || "personal"];
89049
89614
  const account = JSON.stringify(scope2);
89050
- const directory = join18(stateRoot, "codex-chat-deletions", createHash22("sha256").update(account).digest("hex"));
89615
+ const directory = join18(stateRoot, "codex-chat-deletions", createHash23("sha256").update(account).digest("hex"));
89051
89616
  if (!existsSync15(directory)) return;
89052
89617
  const release2 = await lockfile5.lock(directory, { retries: 0, stale: 24e4 });
89053
89618
  try {
@@ -89081,14 +89646,14 @@ async function flushCodexChatDeletions(client2, teamId, stateRoot, notice) {
89081
89646
  }
89082
89647
 
89083
89648
  // src/taskCommandDelivery.ts
89084
- import { createHash as createHash23 } from "crypto";
89649
+ import { createHash as createHash24 } from "crypto";
89085
89650
  import { existsSync as existsSync16, readFileSync as readFileSync17, readdirSync as readdirSync9 } from "fs";
89086
89651
  import { join as join19 } from "path";
89087
89652
  import lockfile6 from "proper-lockfile";
89088
89653
  async function flushTaskCommands(client2, teamId, root = resolveStateDir(), onNotice) {
89089
89654
  const session = client2.getSession();
89090
89655
  const scope2 = [session.apiUrl, session.hashedEmail, teamId || "personal"];
89091
- const directory = join19(root, "task-command-delivery", createHash23("sha256").update(JSON.stringify(scope2)).digest("hex"));
89656
+ const directory = join19(root, "task-command-delivery", createHash24("sha256").update(JSON.stringify(scope2)).digest("hex"));
89092
89657
  if (!existsSync16(directory)) return;
89093
89658
  const release2 = await lockfile6.lock(directory, { retries: 0, stale: 24e4 });
89094
89659
  try {
@@ -89198,7 +89763,7 @@ import {
89198
89763
  writeFileSync as writeFileSync13
89199
89764
  } from "fs";
89200
89765
  import { spawn as spawn7 } from "child_process";
89201
- import { createHash as createHash29 } from "crypto";
89766
+ import { createHash as createHash30 } from "crypto";
89202
89767
  import { extname as extname4, join as join26, resolve as resolve16, relative as relative11 } from "path";
89203
89768
 
89204
89769
  // src/projectSourceRootPolicy.ts
@@ -89233,7 +89798,7 @@ function isEqualOrInside(parent, candidate) {
89233
89798
  }
89234
89799
 
89235
89800
  // src/projectPathPolicy.ts
89236
- import { createHash as createHash24, randomBytes as randomBytes12 } from "crypto";
89801
+ import { createHash as createHash25, randomBytes as randomBytes12 } from "crypto";
89237
89802
  import {
89238
89803
  chmodSync as chmodSync9,
89239
89804
  closeSync as closeSync5,
@@ -89514,7 +90079,7 @@ function isInside2(root, candidate) {
89514
90079
  return relation === "" || !relation.startsWith("..") && !isAbsolute5(relation);
89515
90080
  }
89516
90081
  function sha2563(value) {
89517
- return createHash24("sha256").update(value).digest("hex");
90082
+ return createHash25("sha256").update(value).digest("hex");
89518
90083
  }
89519
90084
  function stableJson2(value) {
89520
90085
  return JSON.stringify(value, (_key, item) => {
@@ -89524,7 +90089,7 @@ function stableJson2(value) {
89524
90089
  }
89525
90090
 
89526
90091
  // src/remoteFileWrites.ts
89527
- import { createHash as createHash25, randomBytes as randomBytes13 } from "crypto";
90092
+ import { createHash as createHash26, randomBytes as randomBytes13 } from "crypto";
89528
90093
  import {
89529
90094
  chmodSync as chmodSync10,
89530
90095
  closeSync as closeSync7,
@@ -89789,7 +90354,7 @@ var MAX_FILE_BYTES2 = 200 * 1024;
89789
90354
  var LOCK_ROOT = join22(tmpdir5(), `openmates-remote-file-locks-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
89790
90355
  var DEFAULT_JOURNAL_ROOT = join22(tmpdir5(), `openmates-remote-file-journal-${typeof process.getuid === "function" ? process.getuid() : "user"}`);
89791
90356
  function sha256ProjectFile(content) {
89792
- return createHash25("sha256").update(content).digest("hex");
90357
+ return createHash26("sha256").update(content).digest("hex");
89793
90358
  }
89794
90359
  async function executeRemoteFileMutation(options) {
89795
90360
  const mutation = validateMutation(options.mutation);
@@ -90477,13 +91042,13 @@ import { existsSync as existsSync23, realpathSync as realpathSync10, statSync as
90477
91042
  import { basename as basename7, dirname as dirname13, relative as relative10, resolve as resolve15 } from "path";
90478
91043
 
90479
91044
  // src/remoteCommandRuntime.ts
90480
- import { createHash as createHash27, randomUUID as randomUUID21 } from "crypto";
91045
+ import { createHash as createHash28, randomUUID as randomUUID22 } from "crypto";
90481
91046
  import { spawn as spawn6, spawnSync as spawnSync4 } from "child_process";
90482
91047
  import { existsSync as existsSync21, lstatSync as lstatSync9, readdirSync as readdirSync12, realpathSync as realpathSync7, statSync as statSync6 } from "fs";
90483
91048
  import { isAbsolute as isAbsolute8, join as join23, relative as relative7, resolve as resolve14, sep } from "path";
90484
91049
 
90485
91050
  // src/remoteCommandAppArmor.ts
90486
- import { createHash as createHash26 } from "crypto";
91051
+ import { createHash as createHash27 } from "crypto";
90487
91052
  import { spawn as spawn5 } from "child_process";
90488
91053
  import { lstatSync as lstatSync8, readFileSync as readFileSync19, realpathSync as realpathSync6, statSync as statSync5 } from "fs";
90489
91054
  import { isAbsolute as isAbsolute7, relative as relative6 } from "path";
@@ -90700,7 +91265,7 @@ function hasControl2(value) {
90700
91265
  });
90701
91266
  }
90702
91267
  function sha2564(value) {
90703
- return createHash26("sha256").update(value).digest("hex");
91268
+ return createHash27("sha256").update(value).digest("hex");
90704
91269
  }
90705
91270
 
90706
91271
  // src/remoteCommandRuntime.ts
@@ -91104,7 +91669,7 @@ var RemoteCommandRuntime = class {
91104
91669
  };
91105
91670
  function createRemoteCommandPreflight(request) {
91106
91671
  if (!request || typeof request !== "object") throw new RemoteCommandError("invalid_request", "Command request must be an object");
91107
- const executionId = request.execution_id ?? randomUUID21();
91672
+ const executionId = request.execution_id ?? randomUUID22();
91108
91673
  if (typeof executionId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(executionId)) throw new RemoteCommandError("invalid_request", "Invalid execution ID");
91109
91674
  if (typeof request.project_id !== "string" || !request.project_id || request.project_id.length > 256) throw new RemoteCommandError("invalid_request", "Invalid Project ID");
91110
91675
  if (typeof request.source_root !== "string" || !request.source_root) throw new RemoteCommandError("invalid_request", "Invalid Project source root");
@@ -91541,7 +92106,7 @@ function stableJson4(value) {
91541
92106
  return JSON.stringify(value);
91542
92107
  }
91543
92108
  function sha2565(value) {
91544
- return createHash27("sha256").update(value).digest("hex");
92109
+ return createHash28("sha256").update(value).digest("hex");
91545
92110
  }
91546
92111
  function deepFreeze(value) {
91547
92112
  if (value && typeof value === "object") {
@@ -91819,7 +92384,7 @@ function capabilityError(message) {
91819
92384
  }
91820
92385
 
91821
92386
  // src/remoteCommandResourceGrants.ts
91822
- import { createHash as createHash28 } from "crypto";
92387
+ import { createHash as createHash29 } from "crypto";
91823
92388
  import { chmodSync as chmodSync12, existsSync as existsSync22, lstatSync as lstatSync10, mkdirSync as mkdirSync18, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync9, statSync as statSync8, writeFileSync as writeFileSync12 } from "fs";
91824
92389
  import { isAbsolute as isAbsolute10, join as join25, relative as relative9 } from "path";
91825
92390
  var STORE_FILE = "remote-command-resource-grants.json";
@@ -91914,7 +92479,7 @@ function resolveRemoteCommandResourceGrants(context) {
91914
92479
  return { writable_targets, network_profiles, credential_profiles };
91915
92480
  }
91916
92481
  function definitionDigest(kind, definition) {
91917
- return createHash28("sha256").update(JSON.stringify(["openmates-command-resource-v1", kind, definition])).digest("hex");
92482
+ return createHash29("sha256").update(JSON.stringify(["openmates-command-resource-v1", kind, definition])).digest("hex");
91918
92483
  }
91919
92484
  function upsert(grant) {
91920
92485
  validateIdentity(grant.project_id, grant.source_id, grant.profile_id);
@@ -93084,7 +93649,7 @@ function readRemoteAccessTextFile(options) {
93084
93649
  truncated,
93085
93650
  sizeBytes: stats.size,
93086
93651
  lineCount: countTextLines(content),
93087
- expected_base: truncated ? null : createHash29("sha256").update(bytes2.subarray(0, bytesRead)).digest("hex")
93652
+ expected_base: truncated ? null : createHash30("sha256").update(bytes2.subarray(0, bytesRead)).digest("hex")
93088
93653
  };
93089
93654
  } catch (error) {
93090
93655
  const code = error.code;
@@ -93134,7 +93699,7 @@ function readRemoteAccessImageChunk(options) {
93134
93699
  mime_type: mimeType,
93135
93700
  size_bytes: stats.size,
93136
93701
  offset: options.offset,
93137
- content_hash: createHash29("sha256").update(bytes2).digest("hex")
93702
+ content_hash: createHash30("sha256").update(bytes2).digest("hex")
93138
93703
  };
93139
93704
  } catch (error) {
93140
93705
  if (error.code === "ELOOP") throw new Error("Remote source path is a symbolic link");
@@ -93182,8 +93747,8 @@ function readRemoteAccessFileChunk(options) {
93182
93747
  content_base64: bytes2.toString("base64"),
93183
93748
  size_bytes: stats.size,
93184
93749
  offset: options.offset,
93185
- file_identity: createHash29("sha256").update(`${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`).digest("hex"),
93186
- chunk_hash: createHash29("sha256").update(bytes2).digest("hex")
93750
+ file_identity: createHash30("sha256").update(`${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`).digest("hex"),
93751
+ chunk_hash: createHash30("sha256").update(bytes2).digest("hex")
93187
93752
  };
93188
93753
  } catch (error) {
93189
93754
  if (error.code === "ELOOP") throw new Error("Remote source path is a symbolic link");
@@ -94745,7 +95310,7 @@ function writeWorkRecoveryAtomically(path2, document) {
94745
95310
 
94746
95311
  // src/revolutBusinessCertificate.ts
94747
95312
  import { execFileSync as execFileSync6 } from "child_process";
94748
- import { createSign, randomUUID as randomUUID22 } from "crypto";
95313
+ import { createSign, randomUUID as randomUUID23 } from "crypto";
94749
95314
  import { chmodSync as chmodSync14, existsSync as existsSync25, mkdirSync as mkdirSync22, readFileSync as readFileSync23 } from "fs";
94750
95315
  import { homedir as homedir8 } from "os";
94751
95316
  import { join as join28, resolve as resolve17 } from "path";
@@ -94819,7 +95384,7 @@ function generateRevolutBusinessClientAssertion(options) {
94819
95384
  aud: REVOLUT_BUSINESS_AUDIENCE,
94820
95385
  exp: now + 3600,
94821
95386
  iat: now,
94822
- jti: randomUUID22()
95387
+ jti: randomUUID23()
94823
95388
  });
94824
95389
  const unsigned = `${header2}.${payload}`;
94825
95390
  const signer = createSign("RSA-SHA256");
@@ -96291,7 +96856,7 @@ async function resolveAddToProjectStorageChoice(client2, project, flags, objectT
96291
96856
  async function createEncryptedProjectItem(client2, project, input) {
96292
96857
  const timestamp = nowSeconds3();
96293
96858
  return client2.createProjectItem(project.projectId, {
96294
- project_item_id: randomUUID23(),
96859
+ project_item_id: randomUUID24(),
96295
96860
  folder_id: input.folderId ?? null,
96296
96861
  item_type: input.itemType,
96297
96862
  target_id: input.targetId,
@@ -96490,7 +97055,7 @@ async function handlePlans(client2, subcommand, rest, flags, redactor) {
96490
97055
  const encryptedSources = proof.length ? await encryptPlanAssumptionSources(plan, masterKey, proof) : void 0;
96491
97056
  if (action === "create" || action === "add") {
96492
97057
  const assumption = await client2.createPlanAssumption(plan.planId, {
96493
- assumption_id: typeof flags.id === "string" ? flags.id : randomUUID23(),
97058
+ assumption_id: typeof flags.id === "string" ? flags.id : randomUUID24(),
96494
97059
  encrypted_text: await encryptPlanAssumptionSources(plan, masterKey, requiredStringFlag(flags.text ?? rest.slice(2).join(" "), "--text <assumption>")),
96495
97060
  status: typeof flags.status === "string" ? flags.status : void 0,
96496
97061
  linked_sub_chat_id: typeof flags["sub-chat"] === "string" ? flags["sub-chat"] : void 0,
@@ -96532,7 +97097,7 @@ async function handlePlans(client2, subcommand, rest, flags, redactor) {
96532
97097
  if (action === "diff") throw new Error("Revision diffs are reviewed in the authenticated web review page; use submit-for-review to print its URL.");
96533
97098
  if (action !== "submit-for-review") throw new Error("Usage: openmates plans revisions list|submit-for-review|status|diff <plan-id>");
96534
97099
  const snapshot2 = JSON.stringify(planToJson(plan));
96535
- const revision = await client2.submitPlanRevision(plan.planId, { fingerprint: createHash30("sha256").update(snapshot2).digest("hex"), encrypted_snapshot: await encryptPlanAssumptionSources(plan, masterKey, snapshot2), created_at: nowSeconds3() });
97100
+ const revision = await client2.submitPlanRevision(plan.planId, { fingerprint: createHash31("sha256").update(snapshot2).digest("hex"), encrypted_snapshot: await encryptPlanAssumptionSources(plan, masterKey, snapshot2), created_at: nowSeconds3() });
96536
97101
  const reviewUrl = `${deriveAppUrl(client2.apiUrl)}/plans/${plan.planId}/review?revision=${revision.revision_id}`;
96537
97102
  if (flags.json === true) printJson2({ revision, review_url: reviewUrl });
96538
97103
  else console.log(`Plan revision submitted for web review: ${reviewUrl}`);
@@ -97426,7 +97991,7 @@ async function handleProjects(client2, subcommand, rest, flags, redactor) {
97426
97991
  const writeMode = await resolveProjectWriteMode(flags, `Choose how OpenMates may change files in ${JSON.stringify(name)}:`);
97427
97992
  const projectKey = randomBytes14(32);
97428
97993
  const wrapping = await client2.projectWrappingKey(context);
97429
- const projectId = randomUUID23();
97994
+ const projectId = randomUUID24();
97430
97995
  const timestamp = nowSeconds3();
97431
97996
  const defaultFocus = buildProjectDefaultFocus(name);
97432
97997
  const slugMetadata = await buildEncryptedObjectSlugMetadata({
@@ -97452,7 +98017,7 @@ async function handleProjects(client2, subcommand, rest, flags, redactor) {
97452
98017
  encrypted_settings: await encryptWithAesGcmCombined(JSON.stringify({ default_focus: defaultFocus }), projectKey),
97453
98018
  key_wrappers: wrapping.teamId ? [{
97454
98019
  key_type: "team",
97455
- hashed_team_id: createHash30("sha256").update(wrapping.teamId).digest("hex"),
98020
+ hashed_team_id: createHash31("sha256").update(wrapping.teamId).digest("hex"),
97456
98021
  team_key_epoch: 1,
97457
98022
  encrypted_project_key: await encryptBytesWithAesGcm(projectKey, wrapping.key),
97458
98023
  wrapper_version: 1,
@@ -97692,7 +98257,7 @@ async function handleProjects(client2, subcommand, rest, flags, redactor) {
97692
98257
  const proposal = isShortWorkspaceAsk(instruction) ? { name: instruction, description: "", icon: "folder", color: "default" } : extractRecord(await client2.planProjectAsk({ instruction }), "proposed_project");
97693
98258
  const projectKey = randomBytes14(32);
97694
98259
  const timestamp = Math.floor(Date.now() / 1e3);
97695
- const projectId = randomUUID23();
98260
+ const projectId = randomUUID24();
97696
98261
  const name = requiredString(proposal, "name", instruction);
97697
98262
  const slugMetadata = await buildEncryptedObjectSlugMetadata({
97698
98263
  value: typeof flags.slug === "string" ? flags.slug : name,
@@ -97815,8 +98380,8 @@ async function handleProjectFiles(client2, masterKey, rest, flags, context) {
97815
98380
  ...flags["include-ignored"] === true ? {
97816
98381
  approvedIgnoredRead: {
97817
98382
  path: String(argumentsValue.path),
97818
- chatId: `cli-direct-${randomUUID23()}`,
97819
- operationId: `cli-direct-${randomUUID23()}`
98383
+ chatId: `cli-direct-${randomUUID24()}`,
98384
+ operationId: `cli-direct-${randomUUID24()}`
97820
98385
  }
97821
98386
  } : {}
97822
98387
  });
@@ -98604,7 +99169,7 @@ async function handleRemoteAccess(client2, subcommand, rest, flags) {
98604
99169
  const projects = await loadProjects(client2, masterKey, hostingFlags, hostingContext);
98605
99170
  const bindings = await resolveRemoteAccessBindings(client2, masterKey, projects, candidateRoots, hostingFlags, hostingContext);
98606
99171
  if (bindings.length === 0) throw new Error("No Project folders were approved for remote access.");
98607
- const sourceSessionId = randomUUID23();
99172
+ const sourceSessionId = randomUUID24();
98608
99173
  const controller = new AbortController();
98609
99174
  const stop = () => controller.abort();
98610
99175
  process.once("SIGINT", stop);
@@ -98758,7 +99323,7 @@ async function resolveRemoteAccessBindings(client2, masterKey, projects, candida
98758
99323
  updated_at: nowSeconds3()
98759
99324
  }, context);
98760
99325
  }
98761
- resolved.push({ rootPath, project, sourceId: randomUUID23() });
99326
+ resolved.push({ rootPath, project, sourceId: randomUUID24() });
98762
99327
  }
98763
99328
  }
98764
99329
  const bindings = [];
@@ -98802,7 +99367,7 @@ async function resolveRemoteAccessBindings(client2, masterKey, projects, candida
98802
99367
  }
98803
99368
  async function createEncryptedRemoteAccessProject(client2, name, rootPath, writeMode, context) {
98804
99369
  const projectKey = randomBytes14(32);
98805
- const projectId = randomUUID23();
99370
+ const projectId = randomUUID24();
98806
99371
  const timestamp = Math.floor(Date.now() / 1e3);
98807
99372
  const wrapping = await client2.projectWrappingKey(context);
98808
99373
  const importedInstructions = readRootProjectInstructions(rootPath);
@@ -98828,7 +99393,7 @@ async function createEncryptedRemoteAccessProject(client2, name, rootPath, write
98828
99393
  encrypted_settings: await encryptWithAesGcmCombined(JSON.stringify({ default_focus: defaultFocus }), projectKey),
98829
99394
  key_wrappers: wrapping.teamId ? [{
98830
99395
  key_type: "team",
98831
- hashed_team_id: createHash30("sha256").update(wrapping.teamId).digest("hex"),
99396
+ hashed_team_id: createHash31("sha256").update(wrapping.teamId).digest("hex"),
98832
99397
  team_key_epoch: 1,
98833
99398
  encrypted_project_key: await encryptBytesWithAesGcm(projectKey, wrapping.key),
98834
99399
  wrapper_version: 1,
@@ -98886,7 +99451,7 @@ async function ensureProjectDefaultFocus(client2, project, context, rootPath) {
98886
99451
  function buildProjectDefaultFocus(projectName, instructions, source = "generated") {
98887
99452
  const name = projectName.trim() || "Untitled project";
98888
99453
  return {
98889
- focus_id: randomUUID23(),
99454
+ focus_id: randomUUID24(),
98890
99455
  name: `Work on ${name}`,
98891
99456
  instructions: instructions?.trim() || `Help with work in ${name}. Follow the user's instructions and the Project's connected source guidance.`,
98892
99457
  source
@@ -100219,7 +100784,7 @@ function normalizeApiKeyLoadedChat(payload) {
100219
100784
  const messages = rawMessages.map((message) => {
100220
100785
  const messageId = String(message.client_message_id ?? message.message_id ?? message.id ?? "");
100221
100786
  const directEmbedIds = Array.isArray(message.embedIds) ? message.embedIds : Array.isArray(message.embed_ids) ? message.embed_ids : [];
100222
- const hashedMessageId = messageId ? createHash30("sha256").update(messageId).digest("hex") : null;
100787
+ const hashedMessageId = messageId ? createHash31("sha256").update(messageId).digest("hex") : null;
100223
100788
  const matchedEmbedIds = hashedMessageId ? rawEmbeds.filter((embed) => embed.hashed_message_id === hashedMessageId && !embed.parent_embed_id).map((embed) => String(embed.embed_id ?? embed.id ?? "")).filter(Boolean) : [];
100224
100789
  return {
100225
100790
  id: messageId,
@@ -102416,7 +102981,7 @@ async function runAccountImport(client2, parserFormat, file, flags) {
102416
102981
  if (flags.yes !== true) {
102417
102982
  await confirmOrExit(`Import ${selectedCount} chat(s) into new encrypted OpenMates chats? [y/N] `);
102418
102983
  }
102419
- const importId = typeof preview.import_id === "string" ? preview.import_id : randomUUID23();
102984
+ const importId = typeof preview.import_id === "string" ? preview.import_id : randomUUID24();
102420
102985
  const selectedChats = parsed.chats.slice(0, selectedCount);
102421
102986
  const selectedFingerprints = selectedChats.map((chat) => chat.source_fingerprint);
102422
102987
  const confirmation = await client2.confirmAccountImport(importId, selectedFingerprints);
@@ -102910,8 +103475,8 @@ async function handleFinance(client2, subcommand, _rest, flags) {
102910
103475
  refresh_token_envelope: connectedAccount.refreshTokenBundle
102911
103476
  }
102912
103477
  ] : [],
102913
- chatId: `cli-finance-${randomUUID23()}`,
102914
- messageId: `cli-finance-message-${randomUUID23()}`,
103478
+ chatId: `cli-finance-${randomUUID24()}`,
103479
+ messageId: `cli-finance-message-${randomUUID24()}`,
102915
103480
  apiKey: resolveApiKey(flags) ?? void 0,
102916
103481
  promptInjectionProtection: false
102917
103482
  });