@orangecollective/oc 0.1.2 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +19 -4
  2. package/dist/oc.js +1001 -108
  3. package/package.json +1 -1
package/dist/oc.js CHANGED
@@ -13,7 +13,7 @@ import open from "open";
13
13
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
14
14
  import { homedir } from "node:os";
15
15
  import path from "node:path";
16
- var VERSION = true ? "0.1.2" : "0.0.0-dev";
16
+ var VERSION = true ? "0.3.0" : "0.0.0-dev";
17
17
  function isOlderThan(version, floor) {
18
18
  const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
19
19
  const [a, b] = [parse(version), parse(floor)];
@@ -24,6 +24,7 @@ function isOlderThan(version, floor) {
24
24
  }
25
25
  var DEFAULT_HOST = "https://www.orangecollective.vc";
26
26
  var MODELS = [
27
+ { provider: "anthropic", id: "claude-haiku-4-5", label: "Claude Haiku 4.5", short: "Haiku 4.5" },
27
28
  { provider: "anthropic", id: "claude-fable-5-1", label: "Claude Fable 5.1", short: "Fable 5.1" },
28
29
  { provider: "anthropic", id: "claude-opus-5-5", label: "Claude Opus 5.5", short: "Opus 5.5" },
29
30
  { provider: "anthropic", id: "claude-sonnet-5", label: "Claude Sonnet 5", short: "Sonnet 5" },
@@ -31,7 +32,7 @@ var MODELS = [
31
32
  { provider: "openai", id: "gpt-6-sol", label: "GPT-6 Sol", short: "GPT-6 Sol" },
32
33
  { provider: "openai", id: "gpt-6-luna", label: "GPT-6 Luna", short: "GPT-6 Luna" }
33
34
  ];
34
- var DEFAULT_MODEL_ID = "gpt-6-sol";
35
+ var DEFAULT_MODEL_ID = "claude-haiku-4-5";
35
36
  var configDir = () => process.env.OC_CONFIG_DIR ?? path.join(homedir(), ".oc");
36
37
  var credentialsPath = () => path.join(configDir(), "credentials.json");
37
38
  var statePath = () => path.join(configDir(), "state.json");
@@ -323,6 +324,16 @@ var OcClient = class {
323
324
  }
324
325
  host;
325
326
  getToken;
327
+ /** Read a JSON error body into a thrown Error, falling back to the status. */
328
+ async fail(res, fallback) {
329
+ let detail = null;
330
+ try {
331
+ const json = await res.json();
332
+ if (typeof json.error === "string") detail = json.error;
333
+ } catch {
334
+ }
335
+ throw new Error(detail ?? `${fallback} (HTTP ${res.status})`);
336
+ }
326
337
  async authHeaders() {
327
338
  return {
328
339
  Authorization: `Bearer ${await this.getToken()}`,
@@ -396,13 +407,98 @@ var OcClient = class {
396
407
  companies: "6",
397
408
  founders: "6",
398
409
  skills: "4",
399
- sections: "batches,companies,founders,skills"
410
+ sections: "batches,companies,founders,members,skills"
400
411
  });
401
412
  if (args.teamId != null) params.set("teamId", String(args.teamId));
402
413
  const res = await this.request(`/api/search/context?${params}`, { signal: args.signal });
403
414
  if (!res.ok) return [];
404
415
  return flattenContextResults(await res.json());
405
416
  }
417
+ /**
418
+ * The batch's activity feed (`GET /api/cli/activity`) — with `after`, only
419
+ * events newer than that timestamp, which is how the ticker polls cheaply.
420
+ */
421
+ async activity(args) {
422
+ const params = new URLSearchParams({ teamId: String(args.teamId), batch: args.batch });
423
+ if (args.after) params.set("after", args.after);
424
+ if (args.limit) params.set("limit", String(args.limit));
425
+ const res = await this.request(`/api/cli/activity?${params}`, { signal: args.signal });
426
+ if (!res.ok) return this.fail(res, "Couldn't load activity");
427
+ const json = await res.json();
428
+ return json.items ?? [];
429
+ }
430
+ /** Company-only search, for resolving the name given to `/fav` and `/unfav`. */
431
+ async searchCompanies(query, limit = 8) {
432
+ const params = new URLSearchParams({
433
+ q: query,
434
+ companies: String(limit),
435
+ founders: "0",
436
+ skills: "0",
437
+ sections: "companies"
438
+ });
439
+ const res = await this.request(`/api/search/context?${params}`);
440
+ if (!res.ok) return [];
441
+ const data = await res.json();
442
+ return (data.companies ?? []).map((c) => ({ id: c.id, name: c.name, batch: c.batch }));
443
+ }
444
+ async favoriteFolders(teamId) {
445
+ const res = await this.request(`/api/cli/favorites?teamId=${teamId}`);
446
+ if (!res.ok) return this.fail(res, "Couldn't load your folders");
447
+ const json = await res.json();
448
+ return json.folders ?? [];
449
+ }
450
+ async addFavorite(folderId, companyId) {
451
+ const res = await this.request("/api/cli/favorites", {
452
+ method: "POST",
453
+ headers: { "Content-Type": "application/json" },
454
+ body: JSON.stringify({ folderId, companyId })
455
+ });
456
+ if (!res.ok) return this.fail(res, "Couldn't add the favorite");
457
+ }
458
+ async removeFavorite(folderId, companyId) {
459
+ const res = await this.request("/api/cli/favorites", {
460
+ method: "DELETE",
461
+ headers: { "Content-Type": "application/json" },
462
+ body: JSON.stringify({ folderId, companyId })
463
+ });
464
+ if (!res.ok) return this.fail(res, "Couldn't remove the favorite");
465
+ }
466
+ async createFavoriteFolder(teamId, name) {
467
+ const res = await this.request("/api/cli/favorites/folders", {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify({ teamId, name })
471
+ });
472
+ if (!res.ok) return this.fail(res, "Couldn't create the folder");
473
+ const json = await res.json();
474
+ return json.id;
475
+ }
476
+ /**
477
+ * The presence tick: report that you're here and what you're on, and get back
478
+ * who else is around. One request does both, so the ticker costs one round-trip
479
+ * a minute rather than two.
480
+ */
481
+ async presence(args) {
482
+ const res = await this.request("/api/presence", {
483
+ method: "POST",
484
+ headers: { "Content-Type": "application/json" },
485
+ signal: args.signal,
486
+ body: JSON.stringify({ surface: "cli", label: args.label ?? null, path: args.path ?? null })
487
+ });
488
+ if (!res.ok) return this.fail(res, "Couldn't load presence");
489
+ const json = await res.json();
490
+ return json.members ?? [];
491
+ }
492
+ /** Companies the org has been opening, asking about and starring. */
493
+ async trending(args) {
494
+ const params = new URLSearchParams();
495
+ if (args.batch) params.set("batch", args.batch);
496
+ if (args.limit) params.set("limit", String(args.limit));
497
+ const res = await this.request(`/api/trending?${params}`, { signal: args.signal });
498
+ if (!res.ok) return this.fail(res, "Couldn't load trending");
499
+ const json = await res.json();
500
+ return { companies: json.companies ?? [], days: json.days ?? 7 };
501
+ }
406
502
  /**
407
503
  * Transport for POST /api/chat. Sends exactly the body the web chat panel's
408
504
  * transport sends, so the route needs no CLI-specific branch.
@@ -445,6 +541,13 @@ function flattenContextResults(raw) {
445
541
  label: f.name,
446
542
  iconUrl: f.avatarUrl ?? void 0
447
543
  })),
544
+ ...(data.members ?? []).map((m) => ({
545
+ type: "member",
546
+ rawId: m.userId,
547
+ label: m.name,
548
+ subtitle: "teammate",
549
+ iconUrl: m.avatarUrl ?? m.authAvatarUrl ?? void 0
550
+ })),
448
551
  ...(data.skills ?? []).map((s) => ({
449
552
  type: "skill",
450
553
  rawId: s.slug,
@@ -462,9 +565,18 @@ function buildContextPrefix(items) {
462
565
 
463
566
  // src/ui/App.tsx
464
567
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
465
- import { Box as Box5, Static, Text as Text5, useApp, useInput } from "ink";
568
+ import { Box as Box7, Static, Text as Text7, useApp, useInput } from "ink";
466
569
  import { generateId, readUIMessageStream } from "ai";
467
570
 
571
+ // ../lib/presence-core.ts
572
+ var PRESENCE_TICK_MS = 6e4;
573
+ var PRESENCE_ONLINE_MS = 15e4;
574
+ function isOnline(member, now = Date.now()) {
575
+ const seen = Date.parse(member.lastSeenAt);
576
+ if (Number.isNaN(seen)) return false;
577
+ return now - seen < PRESENCE_ONLINE_MS;
578
+ }
579
+
468
580
  // src/markdown.ts
469
581
  import { Marked } from "marked";
470
582
  import { markedTerminal } from "marked-terminal";
@@ -485,9 +597,118 @@ function renderMarkdown(text) {
485
597
  }
486
598
  }
487
599
 
488
- // src/ui/Input.tsx
600
+ // src/ui/Activity.tsx
489
601
  import { Box, Text } from "ink";
490
602
 
603
+ // ../lib/format-relative-date.ts
604
+ function formatRelativeDate(iso) {
605
+ if (!iso) return "";
606
+ const then = new Date(iso).getTime();
607
+ if (Number.isNaN(then)) return "";
608
+ const diffMs = Date.now() - then;
609
+ const diffSec = Math.floor(diffMs / 1e3);
610
+ if (diffSec < 60) return "just now";
611
+ const diffMin = Math.floor(diffSec / 60);
612
+ if (diffMin < 60) return `${diffMin}m ago`;
613
+ const diffHr = Math.floor(diffMin / 60);
614
+ if (diffHr < 24) return `${diffHr}h ago`;
615
+ const diffDay = Math.floor(diffHr / 24);
616
+ if (diffDay < 7) return `${diffDay}d ago`;
617
+ const d = new Date(then);
618
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
619
+ }
620
+
621
+ // src/activity.ts
622
+ var FIELD_LABELS = {
623
+ one_liner: "one-liner",
624
+ founder_dynamics: "founder dynamics",
625
+ group_partner: "group partner",
626
+ tldr: "TLDR"
627
+ };
628
+ var humanize = (field) => FIELD_LABELS[field] ?? field.replace(/_/g, " ");
629
+ function joinFields(fields) {
630
+ const labels = fields.map(humanize);
631
+ if (labels.length <= 1) return labels[0] ?? "";
632
+ if (labels.length === 2) return `${labels[0]} and ${labels[1]}`;
633
+ return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
634
+ }
635
+ function money(n) {
636
+ if (n == null) return null;
637
+ if (n >= 1e6) {
638
+ const v = n / 1e6;
639
+ return `$${Number.isInteger(v) ? v : v.toFixed(1)}M`;
640
+ }
641
+ if (n >= 1e3) return `$${Math.round(n / 1e3)}K`;
642
+ return `$${n}`;
643
+ }
644
+ var strong = (text) => ({ text, tone: "strong" });
645
+ var dim = (text) => ({ text, tone: "dim" });
646
+ function isSocialActivity(item) {
647
+ return item.actor != null;
648
+ }
649
+ function describeActivity(item) {
650
+ const { company, actor, meta } = item;
651
+ const name = company?.name ?? "a company";
652
+ const batch = company?.batch ? dim(` (${company.batch})`) : null;
653
+ const who = strong(actor?.name ?? meta.memberName ?? "Someone");
654
+ switch (item.eventType) {
655
+ case "yc_company_added":
656
+ return [
657
+ strong(name),
658
+ dim(` was added${company?.batch ? ` to ${company.batch}` : ""}`),
659
+ ...company?.oneLiner ? [dim(` \u2014 ${company.oneLiner}`)] : []
660
+ ];
661
+ case "yc_company_launched":
662
+ return [strong(name), ...batch ? [batch] : [], dim(" published their Launch YC post")];
663
+ case "yc_company_updated": {
664
+ const fields = meta.changedFields ?? [];
665
+ const single = fields.length === 1 ? fields[0] : null;
666
+ const value = single ? meta.changedValues?.[single] : null;
667
+ const old = single ? meta.changedOldValues?.[single] : null;
668
+ let verb;
669
+ if (!fields.length) verb = "was updated";
670
+ else if (single && value && old) verb = `updated their ${humanize(single)}: ${old} \u2192 ${value}`;
671
+ else if (single && value) verb = `updated their ${humanize(single)} to ${value}`;
672
+ else verb = `updated their ${joinFields(fields)}`;
673
+ return [strong(name), ...batch ? [batch] : [], dim(` ${verb}`)];
674
+ }
675
+ case "yc_company_team_size_changed": {
676
+ const span = meta.oldTeamSize != null && meta.newTeamSize != null ? `updated their team size: ${meta.oldTeamSize} \u2192 ${meta.newTeamSize}` : "changed team size";
677
+ return [strong(name), ...batch ? [batch] : [], dim(` ${span}`)];
678
+ }
679
+ case "company_progress_updated":
680
+ return [
681
+ dim("New progress update \xB7 "),
682
+ strong(name),
683
+ ...meta.headline ? [dim(`: ${meta.headline}`)] : []
684
+ ];
685
+ case "investment_tranche_added": {
686
+ const detail = [money(meta.amount), meta.stage].filter(Boolean).join(" \xB7 ");
687
+ return [
688
+ dim(meta.trancheType === "follow_on" ? "Follow-on investment in " : "New investment in "),
689
+ strong(name),
690
+ ...detail ? [dim(` \xB7 ${detail}`)] : []
691
+ ];
692
+ }
693
+ case "shared_folder_favorite_added":
694
+ return [
695
+ who,
696
+ dim(" favorited "),
697
+ strong(name),
698
+ ...meta.folderName ? [dim(" into "), strong(meta.folderName)] : []
699
+ ];
700
+ case "shared_folder_created":
701
+ return [who, dim(" created a shared folder "), strong(meta.folderName ?? "Untitled")];
702
+ case "team_member_added":
703
+ return [who, dim(" joined the team")];
704
+ default:
705
+ return [dim("Activity for "), strong(name)];
706
+ }
707
+ }
708
+ function activityAge(item) {
709
+ return formatRelativeDate(item.createdAt);
710
+ }
711
+
491
712
  // src/ui/theme.ts
492
713
  var ORANGE = {
493
714
  tint: "#ffe2c4",
@@ -507,40 +728,101 @@ var LOGO_RAMP = [
507
728
  ];
508
729
  var ACCENT = ORANGE.base;
509
730
 
731
+ // src/ui/Activity.tsx
732
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
733
+ function Segments({ segments }) {
734
+ return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, index) => /* @__PURE__ */ jsx(Text, { bold: segment.tone === "strong", dimColor: segment.tone === "dim", children: segment.text }, index)) });
735
+ }
736
+ function ActivityBlock({
737
+ heading,
738
+ items
739
+ }) {
740
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
741
+ heading ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: heading }) : null,
742
+ items.map((item) => /* @__PURE__ */ jsxs(Box, { children: [
743
+ /* @__PURE__ */ jsx(Text, { color: isSocialActivity(item) ? ACCENT : void 0, dimColor: !isSocialActivity(item), children: isSocialActivity(item) ? "\u25C6 " : "\xB7 " }),
744
+ /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
745
+ /* @__PURE__ */ jsx(Segments, { segments: describeActivity(item) }),
746
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${activityAge(item)}` })
747
+ ] })
748
+ ] }, item.id))
749
+ ] });
750
+ }
751
+
510
752
  // src/ui/Input.tsx
511
- import { jsx, jsxs } from "react/jsx-runtime";
753
+ import { Box as Box2, Text as Text2 } from "ink";
754
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
512
755
  var PLACEHOLDER = "ask anything \u2014 @ for context, / for commands";
756
+ function HighlightedText({
757
+ value,
758
+ cursor,
759
+ spans,
760
+ dimPlain = false
761
+ }) {
762
+ const points = /* @__PURE__ */ new Set([0, value.length]);
763
+ if (cursor != null) {
764
+ points.add(cursor);
765
+ points.add(Math.min(cursor + 1, value.length));
766
+ }
767
+ for (const span of spans) {
768
+ points.add(span.start);
769
+ points.add(span.end);
770
+ }
771
+ const edges = [...points].sort((a, b) => a - b);
772
+ const runs = [];
773
+ for (let i = 0; i < edges.length - 1; i++) {
774
+ const [start, end] = [edges[i], edges[i + 1]];
775
+ if (start === end) continue;
776
+ const mention = spans.some((s) => start >= s.start && end <= s.end);
777
+ const caret = cursor != null && start === cursor && end === cursor + 1;
778
+ runs.push(
779
+ /* @__PURE__ */ jsx2(
780
+ Text2,
781
+ {
782
+ color: mention ? ACCENT : void 0,
783
+ bold: mention,
784
+ inverse: caret,
785
+ dimColor: !mention && dimPlain,
786
+ children: value.slice(start, end)
787
+ },
788
+ start
789
+ )
790
+ );
791
+ }
792
+ if (cursor != null && cursor >= value.length) {
793
+ runs.push(
794
+ /* @__PURE__ */ jsx2(Text2, { inverse: true, children: " " }, "caret")
795
+ );
796
+ }
797
+ return /* @__PURE__ */ jsx2(Fragment2, { children: runs });
798
+ }
513
799
  function Input({
514
800
  value,
515
801
  cursor,
516
- context,
802
+ spans,
517
803
  busy,
518
804
  status,
519
- hint
805
+ hint,
806
+ right
520
807
  }) {
521
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
522
- context.length > 0 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
523
- /* @__PURE__ */ jsx(Text, { color: ACCENT, children: "\u23F5 " }),
524
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: context.map((c) => c.label).join(", ") })
525
- ] }) : null,
526
- /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: busy ? "gray" : ACCENT, paddingX: 1, children: [
527
- /* @__PURE__ */ jsx(Text, { color: busy ? "gray" : ACCENT, children: "> " }),
528
- value.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: PLACEHOLDER }) : /* @__PURE__ */ jsxs(Text, { children: [
529
- value.slice(0, cursor),
530
- /* @__PURE__ */ jsx(Text, { inverse: true, children: value[cursor] ?? " " }),
531
- value.slice(cursor + 1)
532
- ] })
808
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
809
+ /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? "gray" : ACCENT, paddingX: 1, children: [
810
+ /* @__PURE__ */ jsx2(Text2, { color: busy ? "gray" : ACCENT, children: "> " }),
811
+ value.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: PLACEHOLDER }) : /* @__PURE__ */ jsx2(Text2, { children: /* @__PURE__ */ jsx2(HighlightedText, { value, cursor, spans }) })
533
812
  ] }),
534
- /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
535
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: status }),
536
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: hint ? ` ${hint}` : "" })
813
+ /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 1, children: [
814
+ /* @__PURE__ */ jsx2(Box2, { flexGrow: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, wrap: "truncate-end", children: [
815
+ status,
816
+ hint ? ` ${hint}` : ""
817
+ ] }) }),
818
+ right ? /* @__PURE__ */ jsx2(Box2, { flexShrink: 0, children: right }) : null
537
819
  ] })
538
820
  ] });
539
821
  }
540
822
 
541
823
  // src/ui/Logo.tsx
542
- import { Box as Box2, Text as Text2 } from "ink";
543
- import { jsx as jsx2 } from "react/jsx-runtime";
824
+ import { Box as Box3, Text as Text3 } from "ink";
825
+ import { jsx as jsx3 } from "react/jsx-runtime";
544
826
  var LOGO_ROWS = [
545
827
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
546
828
  "\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551",
@@ -551,17 +833,17 @@ var LOGO_ROWS = [
551
833
  ];
552
834
  var LOGO_WIDTH = 43;
553
835
  function CompactLogo() {
554
- return /* @__PURE__ */ jsx2(Text2, { bold: true, color: ORANGE.base, children: "oc-cli" });
836
+ return /* @__PURE__ */ jsx3(Text3, { bold: true, color: ORANGE.base, children: "oc-cli" });
555
837
  }
556
838
  function Logo() {
557
839
  const columns = process.stdout.columns || 80;
558
- if (columns < LOGO_WIDTH + 2) return /* @__PURE__ */ jsx2(CompactLogo, {});
559
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: LOGO_ROWS.map((row, index) => /* @__PURE__ */ jsx2(Text2, { color: LOGO_RAMP[index], children: row }, row)) });
840
+ if (columns < LOGO_WIDTH + 2) return /* @__PURE__ */ jsx3(CompactLogo, {});
841
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: LOGO_ROWS.map((row, index) => /* @__PURE__ */ jsx3(Text3, { color: LOGO_RAMP[index], children: row }, row)) });
560
842
  }
561
843
 
562
844
  // src/ui/Picker.tsx
563
- import { Box as Box3, Text as Text3 } from "ink";
564
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
845
+ import { Box as Box4, Text as Text4 } from "ink";
846
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
565
847
  var MAX_VISIBLE = 10;
566
848
  var MAX_LABEL_PAD = 28;
567
849
  function Picker({
@@ -576,22 +858,130 @@ function Picker({
576
858
  visible.reduce((width, row) => Math.max(width, row.label.length), 0)
577
859
  );
578
860
  if (rows.length === 0) {
579
- return status ? /* @__PURE__ */ jsx3(Box3, { paddingLeft: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: status }) }) : null;
861
+ return status ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: status }) }) : null;
580
862
  }
581
- return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: visible.map((row) => {
863
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: visible.map((row) => {
582
864
  const isSelected = rows[selected]?.key === row.key;
583
- return /* @__PURE__ */ jsxs2(Box3, { paddingLeft: 1, children: [
584
- /* @__PURE__ */ jsx3(Text3, { color: isSelected ? ACCENT : void 0, children: isSelected ? "\u276F " : " " }),
585
- /* @__PURE__ */ jsx3(Text3, { color: isSelected ? ACCENT : void 0, bold: isSelected, children: row.label }),
586
- row.meta ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `${" ".repeat(Math.max(1, labelPad - row.label.length))} ${row.meta}` }) : null
865
+ return /* @__PURE__ */ jsxs3(Box4, { paddingLeft: 1, children: [
866
+ /* @__PURE__ */ jsx4(Text4, { color: isSelected ? ACCENT : void 0, children: isSelected ? "\u276F " : " " }),
867
+ /* @__PURE__ */ jsx4(Text4, { color: isSelected ? ACCENT : void 0, bold: isSelected, children: row.label }),
868
+ row.meta ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `${" ".repeat(Math.max(1, labelPad - row.label.length))} ${row.meta}` }) : null
587
869
  ] }, row.key);
588
870
  }) });
589
871
  }
590
872
 
873
+ // src/ui/Social.tsx
874
+ import { Box as Box5, Text as Text5 } from "ink";
875
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
876
+ var STRIP_MAX = 3;
877
+ function stripParts(members) {
878
+ const others = members.filter((m) => !m.isSelf);
879
+ const shown = others.slice(0, STRIP_MAX);
880
+ return { others, shown, extra: others.length - shown.length };
881
+ }
882
+ function presenceStripWidth(members) {
883
+ const { others, shown, extra } = stripParts(members);
884
+ if (others.length === 0) return 0;
885
+ const names = shown.reduce((total, m) => total + 2 + m.shortName.length + 2, 0);
886
+ return names + (extra > 0 ? `+${extra} `.length : 0) + `${others.length + 1} online`.length;
887
+ }
888
+ function PresenceStrip({ members }) {
889
+ const { others, shown, extra } = stripParts(members);
890
+ if (others.length === 0) return null;
891
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
892
+ shown.map((member) => {
893
+ const online = isOnline(member);
894
+ return /* @__PURE__ */ jsxs4(Text5, { color: online ? ACCENT : void 0, dimColor: !online, children: [
895
+ online ? "\u25CF " : "\u25CB ",
896
+ member.shortName.toLowerCase(),
897
+ " "
898
+ ] }, member.userId);
899
+ }),
900
+ extra > 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `+${extra} ` }) : null,
901
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `${others.length + 1} online` })
902
+ ] });
903
+ }
904
+ function PresenceBlock({ members }) {
905
+ if (members.length === 0) {
906
+ return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Nobody else is around right now." }) });
907
+ }
908
+ const width = Math.max(
909
+ 12,
910
+ ...members.map((m) => m.name.length + (m.isSelf ? 6 : 0))
911
+ );
912
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
913
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "ONLINE" }),
914
+ members.map((member) => {
915
+ const online = isOnline(member);
916
+ const name = `${member.name}${member.isSelf ? " (you)" : ""}`;
917
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
918
+ /* @__PURE__ */ jsx5(Text5, { color: online ? ACCENT : void 0, dimColor: !online, children: online ? "\u25CF " : "\u25CB " }),
919
+ /* @__PURE__ */ jsx5(Text5, { bold: online, children: name.padEnd(width + 2) }),
920
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
921
+ member.viewingLabel ?? "\u2014",
922
+ member.surface === "cli" ? " \xB7 cli" : ""
923
+ ] })
924
+ ] }, member.userId);
925
+ })
926
+ ] });
927
+ }
928
+ function SocialBlock({ lines }) {
929
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: lines.map((line) => /* @__PURE__ */ jsxs4(Box5, { children: [
930
+ /* @__PURE__ */ jsx5(Text5, { color: ACCENT, children: "\u25C6 " }),
931
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: line })
932
+ ] }, line)) });
933
+ }
934
+ var BAR_CELLS = 8;
935
+ function HeatBar({ heat }) {
936
+ const filled = Math.max(1, Math.min(BAR_CELLS, Math.round(heat / 100 * BAR_CELLS)));
937
+ return /* @__PURE__ */ jsxs4(Text5, { children: [
938
+ /* @__PURE__ */ jsx5(Text5, { color: heat >= 66 ? ORANGE.base : heat >= 33 ? ORANGE.deep : ORANGE.shadow, children: "\u2588".repeat(filled) }),
939
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2591".repeat(BAR_CELLS - filled) })
940
+ ] });
941
+ }
942
+ function TrendingBlock({
943
+ companies,
944
+ batch,
945
+ days
946
+ }) {
947
+ if (companies.length === 0) {
948
+ return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
949
+ "Nothing has moved",
950
+ batch ? ` in ${batch}` : "",
951
+ " in the last ",
952
+ days,
953
+ " days."
954
+ ] }) });
955
+ }
956
+ const nameWidth = Math.min(22, Math.max(...companies.map((c) => c.name.length)));
957
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
958
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `TRENDING${batch ? ` \xB7 ${batch}` : ""} \xB7 LAST ${days} DAYS` }),
959
+ companies.map((company, index) => {
960
+ const climb = company.prevRank != null ? company.prevRank - company.rank : null;
961
+ const detail = [
962
+ company.views > 0 && `${company.views} opened`,
963
+ company.chats > 0 && `${company.chats} asked`,
964
+ company.favorites > 0 && `${company.favorites} starred`
965
+ ].filter(Boolean).join(" \xB7 ");
966
+ return /* @__PURE__ */ jsxs4(Box5, { children: [
967
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `${String(index + 1).padStart(2)} ` }),
968
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: truncate(company.name, nameWidth).padEnd(nameWidth + 2) }),
969
+ /* @__PURE__ */ jsx5(HeatBar, { heat: company.heat }),
970
+ /* @__PURE__ */ jsx5(Text5, { children: String(company.heat).padStart(5) }),
971
+ /* @__PURE__ */ jsx5(Text5, { color: climb != null && climb > 0 ? ACCENT : void 0, dimColor: climb == null || climb <= 0, children: climb == null || climb === 0 ? " \u2014" : `${climb > 0 ? " \u25B2" : " \u25BC"}${String(Math.abs(climb)).padStart(2)}` }),
972
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${detail}` })
973
+ ] }, company.companyId);
974
+ })
975
+ ] });
976
+ }
977
+ function truncate(value, width) {
978
+ return value.length <= width ? value : `${value.slice(0, Math.max(1, width - 1))}\u2026`;
979
+ }
980
+
591
981
  // src/ui/Turn.tsx
592
982
  import { useEffect, useState } from "react";
593
- import { Box as Box4, Text as Text4 } from "ink";
594
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
983
+ import { Box as Box6, Text as Text6 } from "ink";
984
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
595
985
  var SPINNER = ["\u2722", "\u2733", "\u2736", "\u273B", "\u273D", "\u273B", "\u2736", "\u2733"];
596
986
  function useSpinner(active) {
597
987
  const [frame, setFrame] = useState(0);
@@ -604,15 +994,15 @@ function useSpinner(active) {
604
994
  }
605
995
  function ToolRows({ tools, live }) {
606
996
  if (tools.length === 0) return null;
607
- return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: tools.map((tool) => /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
608
- /* @__PURE__ */ jsxs3(Box4, { children: [
609
- /* @__PURE__ */ jsxs3(Text4, { color: tool.state === "error" ? "red" : tool.state === "running" ? ACCENT : "gray", children: [
997
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", children: tools.map((tool) => /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
998
+ /* @__PURE__ */ jsxs5(Box6, { children: [
999
+ /* @__PURE__ */ jsxs5(Text6, { color: tool.state === "error" ? "red" : tool.state === "running" ? ACCENT : "gray", children: [
610
1000
  "\u25CF",
611
1001
  " "
612
1002
  ] }),
613
- /* @__PURE__ */ jsx4(Text4, { bold: tool.state === "running" && live, children: tool.name })
1003
+ /* @__PURE__ */ jsx6(Text6, { bold: tool.state === "running" && live, children: tool.name })
614
1004
  ] }),
615
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1005
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
616
1006
  " \u23BF ",
617
1007
  tool.state === "running" ? "running\u2026" : tool.state === "error" ? "failed" : "done"
618
1008
  ] })
@@ -620,7 +1010,7 @@ function ToolRows({ tools, live }) {
620
1010
  }
621
1011
  function Sources({ sources }) {
622
1012
  if (sources.length === 0) return null;
623
- return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: sources.map((source, index) => /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1013
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginTop: 1, children: sources.map((source, index) => /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
624
1014
  ` ${index + 1}. `,
625
1015
  source.title ? `${source.title} \u2014 ` : "",
626
1016
  source.url
@@ -633,27 +1023,27 @@ function AssistantTurn({
633
1023
  thoughtSeconds
634
1024
  }) {
635
1025
  const body = renderMarkdown(stripContextLeakage(text));
636
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
637
- /* @__PURE__ */ jsx4(ToolRows, { tools, live: false }),
638
- thoughtSeconds != null ? /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1026
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginBottom: 1, children: [
1027
+ /* @__PURE__ */ jsx6(ToolRows, { tools, live: false }),
1028
+ thoughtSeconds != null ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
639
1029
  "\u273B thought for ",
640
1030
  thoughtSeconds,
641
1031
  "s"
642
1032
  ] }) : null,
643
- body ? /* @__PURE__ */ jsx4(Box4, { marginTop: tools.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx4(Text4, { children: body }) }) : null,
644
- /* @__PURE__ */ jsx4(Sources, { sources })
1033
+ body ? /* @__PURE__ */ jsx6(Box6, { marginTop: tools.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx6(Text6, { children: body }) }) : null,
1034
+ /* @__PURE__ */ jsx6(Sources, { sources })
645
1035
  ] });
646
1036
  }
647
1037
  function StreamingTurn({ turn }) {
648
1038
  const body = stripContextLeakage(turn.text);
649
1039
  const reasoningTail = turn.reasoning.trim().split("\n").at(-1) ?? "";
650
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
651
- /* @__PURE__ */ jsx4(ToolRows, { tools: turn.tools, live: true }),
652
- !body && reasoningTail ? /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1040
+ return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginBottom: 1, children: [
1041
+ /* @__PURE__ */ jsx6(ToolRows, { tools: turn.tools, live: true }),
1042
+ !body && reasoningTail ? /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
653
1043
  " ",
654
- truncate(reasoningTail, (process.stdout.columns || 80) - 4)
1044
+ truncate2(reasoningTail, (process.stdout.columns || 80) - 4)
655
1045
  ] }) : null,
656
- body ? /* @__PURE__ */ jsx4(Text4, { children: body }) : null
1046
+ body ? /* @__PURE__ */ jsx6(Text6, { children: body }) : null
657
1047
  ] });
658
1048
  }
659
1049
  function StatusLine({ turn }) {
@@ -666,23 +1056,23 @@ function StatusLine({ turn }) {
666
1056
  const elapsed = Math.max(0, Math.round((now - turn.startedAt) / 1e3));
667
1057
  const running = turn.tools.find((t) => t.state === "running");
668
1058
  const verb = running ? running.name : turn.text ? "Writing" : "Thinking";
669
- return /* @__PURE__ */ jsxs3(Box4, { children: [
670
- /* @__PURE__ */ jsxs3(Text4, { color: ACCENT, children: [
1059
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
1060
+ /* @__PURE__ */ jsxs5(Text6, { color: ACCENT, children: [
671
1061
  spinner,
672
1062
  " "
673
1063
  ] }),
674
- /* @__PURE__ */ jsxs3(Text4, { children: [
1064
+ /* @__PURE__ */ jsxs5(Text6, { children: [
675
1065
  verb,
676
1066
  "\u2026 "
677
1067
  ] }),
678
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1068
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
679
1069
  "(",
680
1070
  elapsed,
681
1071
  "s \xB7 esc to interrupt)"
682
1072
  ] })
683
1073
  ] });
684
1074
  }
685
- function truncate(value, width) {
1075
+ function truncate2(value, width) {
686
1076
  if (width <= 1 || value.length <= width) return value;
687
1077
  return `${value.slice(0, Math.max(1, width - 1))}\u2026`;
688
1078
  }
@@ -697,29 +1087,61 @@ var EMPTY_TURN = {
697
1087
  };
698
1088
 
699
1089
  // src/ui/App.tsx
700
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1090
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
701
1091
  var FLUSH_INTERVAL_MS = 60;
702
1092
  var MENTION_DEBOUNCE_MS = 200;
703
1093
  var EXIT_CONFIRM_MS = 3e3;
1094
+ var ACTIVITY_POLL_MS = 6e4;
1095
+ var ACTIVITY_LIST_DEFAULT = 10;
1096
+ var ACTIVITY_LIST_MAX = 30;
1097
+ var SHORTCUT_TIP = "? for shortcuts";
1098
+ var TRENDING_LIST_DEFAULT = 8;
1099
+ var TRENDING_LIST_MAX = 25;
704
1100
  var COMMANDS = [
705
1101
  { name: "model", args: "[name]", blurb: "switch model" },
706
1102
  { name: "web", blurb: "toggle web search" },
707
1103
  { name: "team", args: "[slug]", blurb: "switch active team" },
708
1104
  { name: "batch", args: "[W26]", blurb: "set the batch for the digest" },
709
1105
  { name: "digest", args: "[-r]", blurb: "re-run the activity digest" },
1106
+ { name: "activity", args: "[n | on | off]", blurb: "latest batch activity, or toggle the ticker" },
1107
+ { name: "who", args: "[on | off]", blurb: "who's online, or toggle presence" },
1108
+ { name: "trending", args: "[n]", blurb: "what the team has been looking at" },
1109
+ { name: "fav", args: "<company> [in folder]", blurb: "add a company to a favorites folder" },
1110
+ { name: "unfav", args: "<company>", blurb: "remove your favorite" },
1111
+ { name: "favs", args: "[folder]", blurb: "list folders, or a folder's companies" },
710
1112
  { name: "new", blurb: "start a fresh thread" },
711
1113
  { name: "clear", blurb: "clear the screen" },
712
1114
  { name: "help", blurb: "show shortcuts" },
713
1115
  { name: "quit", blurb: "exit" }
714
1116
  ];
715
- var SHORTCUTS = ` @name attach a company, founder, batch or skill
1117
+ var SHORTCUTS = ` @name mention a company, founder, batch or skill (stays in your text)
716
1118
  / commands
717
1119
  \u2191 \u2193 history, or move through a list
718
1120
  tab accept a suggestion
719
- esc interrupt \xB7 close a list \xB7 drop context
1121
+ esc interrupt \xB7 close a list
720
1122
  ctrl-a / ctrl-e start / end of line
721
1123
  ctrl-w / ctrl-u delete word / line
722
1124
  ctrl-c \xD72 exit`;
1125
+ function findMentionSpans(text, records) {
1126
+ const spans = [];
1127
+ for (const record of records) {
1128
+ const needle = `@${record.label}`;
1129
+ let from = 0;
1130
+ for (; ; ) {
1131
+ const at = text.indexOf(needle, from);
1132
+ if (at < 0) break;
1133
+ spans.push({ start: at, end: at + needle.length });
1134
+ from = at + needle.length;
1135
+ }
1136
+ }
1137
+ spans.sort((a, b) => a.start - b.start || b.end - a.end);
1138
+ return spans.filter((span, i) => !spans.slice(0, i).some((k) => span.start >= k.start && span.end <= k.end));
1139
+ }
1140
+ function parseFavArgument(argument) {
1141
+ const match = /^(.*\S)\s+(?:in|into)\s+(\S.*)$/.exec(argument);
1142
+ if (match) return { name: match[1].trim(), folder: match[2].trim() };
1143
+ return { name: argument.trim(), folder: null };
1144
+ }
723
1145
  function App({ client, bootstrap, host, state }) {
724
1146
  const { exit } = useApp();
725
1147
  const [transcript, setTranscript] = useState2([]);
@@ -738,6 +1160,16 @@ function App({ client, bootstrap, host, state }) {
738
1160
  const [mentionLoading, setMentionLoading] = useState2(false);
739
1161
  const [mentionSearchedFor, setMentionSearchedFor] = useState2(null);
740
1162
  const [selected, setSelected] = useState2(0);
1163
+ const [modal, setModal] = useState2(null);
1164
+ const [activityOn, setActivityOn] = useState2(state.activityUpdates ?? true);
1165
+ const activityCursorRef = useRef(null);
1166
+ const pendingActivityRef = useRef([]);
1167
+ const [presenceOn, setPresenceOn] = useState2(state.presence ?? true);
1168
+ const [presence, setPresence] = useState2([]);
1169
+ const presenceRef = useRef([]);
1170
+ const seenOnlineRef = useRef(null);
1171
+ const pendingSocialRef = useRef([]);
1172
+ const viewingRef = useRef(null);
741
1173
  const [teamId, setTeamId] = useState2(
742
1174
  state.activeTeamId ?? bootstrap.activeTeamId
743
1175
  );
@@ -746,6 +1178,8 @@ function App({ client, bootstrap, host, state }) {
746
1178
  const [webSearch, setWebSearch] = useState2(state.webSearchEnabled ?? false);
747
1179
  const threadIdRef = useRef(state.threadId ?? generateId());
748
1180
  const messagesRef = useRef([]);
1181
+ const busyRef = useRef(false);
1182
+ busyRef.current = busy;
749
1183
  const turnAbortRef = useRef(null);
750
1184
  const digestAbortRef = useRef(null);
751
1185
  const team = useMemo(
@@ -866,9 +1300,359 @@ function App({ client, bootstrap, host, state }) {
866
1300
  controller.abort();
867
1301
  };
868
1302
  }, [client, mentionQuery, teamId]);
1303
+ const flushPendingActivity = useCallback(() => {
1304
+ const lines = pendingSocialRef.current;
1305
+ if (lines.length > 0) {
1306
+ pendingSocialRef.current = [];
1307
+ push({ kind: "social", lines });
1308
+ }
1309
+ const items = pendingActivityRef.current;
1310
+ if (items.length === 0) return;
1311
+ pendingActivityRef.current = [];
1312
+ push({ kind: "activity", heading: null, items });
1313
+ }, [push]);
1314
+ useEffect2(() => {
1315
+ if (!activityOn || !batch || teamId == null) return;
1316
+ let cancelled = false;
1317
+ const controller = new AbortController();
1318
+ const prime = async () => {
1319
+ if (activityCursorRef.current) return;
1320
+ try {
1321
+ const [latest] = await client.activity({ teamId, batch, limit: 1, signal: controller.signal });
1322
+ activityCursorRef.current = latest?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
1323
+ } catch {
1324
+ if (!cancelled) activityCursorRef.current = (/* @__PURE__ */ new Date()).toISOString();
1325
+ }
1326
+ };
1327
+ const tick = async () => {
1328
+ if (cancelled || !activityCursorRef.current) return;
1329
+ try {
1330
+ const items = await client.activity({
1331
+ teamId,
1332
+ batch,
1333
+ after: activityCursorRef.current,
1334
+ limit: 20,
1335
+ signal: controller.signal
1336
+ });
1337
+ if (cancelled || items.length === 0) return;
1338
+ activityCursorRef.current = items[0].createdAt;
1339
+ const chronological = [...items].reverse();
1340
+ if (busyRef.current) pendingActivityRef.current.push(...chronological);
1341
+ else push({ kind: "activity", heading: null, items: chronological });
1342
+ } catch {
1343
+ }
1344
+ };
1345
+ void prime();
1346
+ const timer = setInterval(() => void tick(), ACTIVITY_POLL_MS);
1347
+ return () => {
1348
+ cancelled = true;
1349
+ controller.abort();
1350
+ clearInterval(timer);
1351
+ };
1352
+ }, [activityOn, batch, client, push, teamId]);
1353
+ useEffect2(() => {
1354
+ if (!presenceOn) {
1355
+ setPresence([]);
1356
+ presenceRef.current = [];
1357
+ seenOnlineRef.current = null;
1358
+ return;
1359
+ }
1360
+ let cancelled = false;
1361
+ const controller = new AbortController();
1362
+ const tick = async () => {
1363
+ if (cancelled) return;
1364
+ try {
1365
+ const members = await client.presence({
1366
+ label: viewingRef.current ?? batch,
1367
+ signal: controller.signal
1368
+ });
1369
+ if (cancelled) return;
1370
+ const previous = presenceRef.current;
1371
+ presenceRef.current = members;
1372
+ setPresence(members);
1373
+ const online = new Set(members.filter((m) => !m.isSelf).map((m) => m.userId));
1374
+ const seen = seenOnlineRef.current;
1375
+ if (seen == null) {
1376
+ seenOnlineRef.current = online;
1377
+ return;
1378
+ }
1379
+ const lines = [];
1380
+ for (const member of members) {
1381
+ if (member.isSelf || seen.has(member.userId)) continue;
1382
+ const where = member.viewingLabel ? ` \xB7 ${member.viewingLabel}` : "";
1383
+ lines.push(`${member.name} came online${where}`);
1384
+ }
1385
+ for (const userId of seen) {
1386
+ if (online.has(userId)) continue;
1387
+ const gone = previous.find((m) => m.userId === userId);
1388
+ lines.push(`${gone?.name ?? "A teammate"} went offline`);
1389
+ }
1390
+ seenOnlineRef.current = online;
1391
+ if (lines.length === 0) return;
1392
+ if (busyRef.current) pendingSocialRef.current.push(...lines);
1393
+ else push({ kind: "social", lines });
1394
+ } catch {
1395
+ }
1396
+ };
1397
+ void tick();
1398
+ const timer = setInterval(() => void tick(), PRESENCE_TICK_MS);
1399
+ return () => {
1400
+ cancelled = true;
1401
+ controller.abort();
1402
+ clearInterval(timer);
1403
+ };
1404
+ }, [batch, client, presenceOn, push]);
1405
+ const showPresence = useCallback(async () => {
1406
+ try {
1407
+ const members = await client.presence({ label: viewingRef.current ?? batch });
1408
+ presenceRef.current = members;
1409
+ setPresence(members);
1410
+ seenOnlineRef.current = new Set(members.filter((m) => !m.isSelf).map((m) => m.userId));
1411
+ push({ kind: "presence", members });
1412
+ } catch (error) {
1413
+ notice(describeError(error), "error");
1414
+ }
1415
+ }, [batch, client, notice, push]);
1416
+ const showTrending = useCallback(
1417
+ async (count) => {
1418
+ try {
1419
+ const { companies, days } = await client.trending({ batch, limit: count });
1420
+ push({ kind: "trending", batch, days, companies });
1421
+ } catch (error) {
1422
+ notice(describeError(error), "error");
1423
+ }
1424
+ },
1425
+ [batch, client, notice, push]
1426
+ );
1427
+ const showRecentActivity = useCallback(
1428
+ async (count) => {
1429
+ if (!batch || teamId == null) {
1430
+ notice("No current batch for this team.", "error");
1431
+ return;
1432
+ }
1433
+ try {
1434
+ const items = await client.activity({ teamId, batch, limit: count });
1435
+ if (items.length === 0) {
1436
+ notice(`No activity yet for ${batch}.`);
1437
+ return;
1438
+ }
1439
+ if (!activityCursorRef.current || items[0].createdAt > activityCursorRef.current) {
1440
+ activityCursorRef.current = items[0].createdAt;
1441
+ }
1442
+ push({
1443
+ kind: "activity",
1444
+ heading: `Latest activity \xB7 ${batch} \xB7 ${team?.name ?? "team"}`,
1445
+ items: [...items].reverse()
1446
+ });
1447
+ } catch (error) {
1448
+ notice(describeError(error), "error");
1449
+ }
1450
+ },
1451
+ [batch, client, notice, push, team?.name, teamId]
1452
+ );
1453
+ const pickFrom = useCallback(
1454
+ (title, rows, defaultIndex = 0) => new Promise((resolve) => {
1455
+ setSelected(Math.max(0, Math.min(defaultIndex, rows.length - 1)));
1456
+ setModal({
1457
+ title,
1458
+ rows,
1459
+ resolve: (key) => {
1460
+ setModal(null);
1461
+ resolve(key);
1462
+ }
1463
+ });
1464
+ }),
1465
+ []
1466
+ );
1467
+ const resolveCompany = useCallback(
1468
+ async (name, attached) => {
1469
+ const mentioned = attached.find((c) => c.type === "company");
1470
+ if (mentioned) {
1471
+ return { id: Number(mentioned.rawId), name: mentioned.label, batch: mentioned.subtitle ?? null };
1472
+ }
1473
+ if (!name) {
1474
+ notice("Name a company \u2014 /fav Lyon, or /fav @Lyon.", "error");
1475
+ return null;
1476
+ }
1477
+ const hits = await client.searchCompanies(name);
1478
+ if (hits.length === 0) {
1479
+ notice(`No company matches \u201C${name}\u201D.`, "error");
1480
+ return null;
1481
+ }
1482
+ const exact = hits.filter((h) => h.name.toLowerCase() === name.toLowerCase());
1483
+ if (exact.length === 1) return exact[0];
1484
+ if (hits.length === 1) return hits[0];
1485
+ const key = await pickFrom(
1486
+ `Which \u201C${name}\u201D?`,
1487
+ hits.map((h) => ({ key: String(h.id), label: h.name, meta: h.batch ?? void 0 }))
1488
+ );
1489
+ return hits.find((h) => String(h.id) === key) ?? null;
1490
+ },
1491
+ [client, notice, pickFrom]
1492
+ );
1493
+ const runFav = useCallback(
1494
+ async (argument, attached) => {
1495
+ if (teamId == null) {
1496
+ notice("Pick a team first \u2014 /team.", "error");
1497
+ return;
1498
+ }
1499
+ const { name, folder: folderName } = parseFavArgument(argument);
1500
+ const company = await resolveCompany(name, attached);
1501
+ if (!company) return;
1502
+ let folders;
1503
+ try {
1504
+ folders = await client.favoriteFolders(teamId);
1505
+ } catch (error) {
1506
+ notice(describeError(error), "error");
1507
+ return;
1508
+ }
1509
+ const me = bootstrap.user.id;
1510
+ const already = (f) => f.items.some((i) => i.companyId === company.id && i.addedBy === me);
1511
+ let folder = null;
1512
+ if (folderName) {
1513
+ const needle = folderName.toLowerCase();
1514
+ folder = folders.find((f) => f.name.toLowerCase() === needle) ?? folders.find((f) => f.name.toLowerCase().includes(needle)) ?? null;
1515
+ if (!folder) {
1516
+ try {
1517
+ const id = await client.createFavoriteFolder(teamId, folderName);
1518
+ folder = { id, name: folderName };
1519
+ notice(`Created folder \u201C${folderName}\u201D in ${team?.name ?? "this team"}.`);
1520
+ } catch (error) {
1521
+ notice(describeError(error), "error");
1522
+ return;
1523
+ }
1524
+ }
1525
+ } else if (folders.length === 0) {
1526
+ notice(
1527
+ `No favorites folders in ${team?.name ?? "this team"} yet \u2014 /fav ${company.name} in ${company.batch ?? batch ?? "My"} favorites creates one.`,
1528
+ "error"
1529
+ );
1530
+ return;
1531
+ } else {
1532
+ const ordered = [...folders].sort((a, b) => Number(b.isOwner) - Number(a.isOwner));
1533
+ const batchKey = (company.batch ?? batch ?? "").toLowerCase();
1534
+ const defaultIndex = Math.max(
1535
+ 0,
1536
+ ordered.findIndex((f) => batchKey && f.name.toLowerCase().startsWith(batchKey))
1537
+ );
1538
+ const key = await pickFrom(
1539
+ `Add ${company.name} to\u2026`,
1540
+ ordered.map((f) => ({
1541
+ key: String(f.id),
1542
+ label: f.name,
1543
+ meta: [
1544
+ f.isOwner ? "mine" : f.ownerName,
1545
+ `${f.items.length} ${f.items.length === 1 ? "company" : "companies"}`,
1546
+ already(f) ? "already added" : null
1547
+ ].filter(Boolean).join(" \xB7 ")
1548
+ })),
1549
+ defaultIndex
1550
+ );
1551
+ if (!key) return;
1552
+ folder = ordered.find((f) => String(f.id) === key) ?? null;
1553
+ if (!folder) return;
1554
+ }
1555
+ if ("items" in folder && already(folder)) {
1556
+ notice(`${company.name} is already in ${folder.name}.`);
1557
+ return;
1558
+ }
1559
+ try {
1560
+ await client.addFavorite(folder.id, company.id);
1561
+ notice(`\u2605 Added ${company.name} to ${folder.name}.`);
1562
+ } catch (error) {
1563
+ notice(describeError(error), "error");
1564
+ }
1565
+ },
1566
+ [batch, bootstrap.user.id, client, notice, pickFrom, resolveCompany, team?.name, teamId]
1567
+ );
1568
+ const runUnfav = useCallback(
1569
+ async (argument, attached) => {
1570
+ if (teamId == null) {
1571
+ notice("Pick a team first \u2014 /team.", "error");
1572
+ return;
1573
+ }
1574
+ const company = await resolveCompany(argument.trim(), attached);
1575
+ if (!company) return;
1576
+ let folders;
1577
+ try {
1578
+ folders = await client.favoriteFolders(teamId);
1579
+ } catch (error) {
1580
+ notice(describeError(error), "error");
1581
+ return;
1582
+ }
1583
+ const me = bootstrap.user.id;
1584
+ const holding = folders.filter(
1585
+ (f) => f.items.some((i) => i.companyId === company.id && i.addedBy === me)
1586
+ );
1587
+ if (holding.length === 0) {
1588
+ notice(`${company.name} isn't in any of your folders in ${team?.name ?? "this team"}.`);
1589
+ return;
1590
+ }
1591
+ let folder = holding[0];
1592
+ if (holding.length > 1) {
1593
+ const key = await pickFrom(
1594
+ `Remove ${company.name} from\u2026`,
1595
+ holding.map((f) => ({ key: String(f.id), label: f.name }))
1596
+ );
1597
+ if (!key) return;
1598
+ folder = holding.find((f) => String(f.id) === key) ?? folder;
1599
+ }
1600
+ try {
1601
+ await client.removeFavorite(folder.id, company.id);
1602
+ notice(`Removed ${company.name} from ${folder.name}.`);
1603
+ } catch (error) {
1604
+ notice(describeError(error), "error");
1605
+ }
1606
+ },
1607
+ [bootstrap.user.id, client, notice, pickFrom, resolveCompany, team?.name, teamId]
1608
+ );
1609
+ const runFavs = useCallback(
1610
+ async (argument) => {
1611
+ if (teamId == null) {
1612
+ notice("Pick a team first \u2014 /team.", "error");
1613
+ return;
1614
+ }
1615
+ let folders;
1616
+ try {
1617
+ folders = await client.favoriteFolders(teamId);
1618
+ } catch (error) {
1619
+ notice(describeError(error), "error");
1620
+ return;
1621
+ }
1622
+ if (folders.length === 0) {
1623
+ notice(`No favorites folders in ${team?.name ?? "this team"} yet.`);
1624
+ return;
1625
+ }
1626
+ const needle = argument.trim().toLowerCase();
1627
+ if (!needle) {
1628
+ const ordered = [...folders].sort((a, b) => Number(b.isOwner) - Number(a.isOwner));
1629
+ const width = Math.max(...ordered.map((f) => f.name.length));
1630
+ notice(
1631
+ ordered.map(
1632
+ (f) => ` \u2605 ${f.name.padEnd(width)} ${f.items.length} ${f.items.length === 1 ? "company" : "companies"} \xB7 ${f.isOwner ? "mine" : f.ownerName}`
1633
+ ).join("\n")
1634
+ );
1635
+ return;
1636
+ }
1637
+ const folder = folders.find((f) => f.name.toLowerCase() === needle) ?? folders.find((f) => f.name.toLowerCase().includes(needle));
1638
+ if (!folder) {
1639
+ notice(`No folder matches \u201C${argument.trim()}\u201D. /favs lists them.`, "error");
1640
+ return;
1641
+ }
1642
+ if (folder.items.length === 0) {
1643
+ notice(`${folder.name} is empty.`);
1644
+ return;
1645
+ }
1646
+ const me = bootstrap.user.id;
1647
+ const seen = /* @__PURE__ */ new Set();
1648
+ const lines = folder.items.filter((i) => seen.has(i.companyId) ? false : (seen.add(i.companyId), true)).sort((a, b) => a.companyName.localeCompare(b.companyName)).map((i) => ` ${i.addedBy === me ? "\u2605" : "\u2606"} ${i.companyName}${i.batch ? ` \xB7 ${i.batch}` : ""}`);
1649
+ notice(`${folder.name}
1650
+ ${lines.join("\n")}`);
1651
+ },
1652
+ [bootstrap.user.id, client, notice, team?.name, teamId]
1653
+ );
869
1654
  const send = useCallback(
870
- async (raw) => {
871
- const attached = context;
1655
+ async (raw, attached) => {
872
1656
  const userMessage = {
873
1657
  id: generateId(),
874
1658
  role: "user",
@@ -877,7 +1661,6 @@ function App({ client, bootstrap, host, state }) {
877
1661
  const outgoing = [...messagesRef.current, userMessage];
878
1662
  messagesRef.current = outgoing;
879
1663
  push({ kind: "user", text: raw, context: attached });
880
- setContext([]);
881
1664
  setBusy(true);
882
1665
  const controller = new AbortController();
883
1666
  turnAbortRef.current = controller;
@@ -934,12 +1717,13 @@ function App({ client, bootstrap, host, state }) {
934
1717
  setLive(null);
935
1718
  setBusy(false);
936
1719
  turnAbortRef.current = null;
1720
+ flushPendingActivity();
937
1721
  }
938
1722
  },
939
- [context, notice, push, transport]
1723
+ [flushPendingActivity, notice, push, transport]
940
1724
  );
941
1725
  const runCommand = useCallback(
942
- (raw) => {
1726
+ (raw, attached) => {
943
1727
  const [command, ...rest] = raw.trim().slice(1).split(/\s+/);
944
1728
  const argument = rest.join(" ").trim();
945
1729
  switch (command) {
@@ -998,6 +1782,7 @@ function App({ client, bootstrap, host, state }) {
998
1782
  return;
999
1783
  }
1000
1784
  setTeamId(match.teamId);
1785
+ activityCursorRef.current = null;
1001
1786
  void patchState({ activeTeamId: match.teamId });
1002
1787
  notice(`Team: ${match.name}`);
1003
1788
  return;
@@ -1009,6 +1794,7 @@ function App({ client, bootstrap, host, state }) {
1009
1794
  }
1010
1795
  const next = argument.toUpperCase();
1011
1796
  setBatch(next);
1797
+ activityCursorRef.current = null;
1012
1798
  void patchState({ batch: next });
1013
1799
  notice(`Batch: ${next}`);
1014
1800
  return;
@@ -1016,37 +1802,110 @@ function App({ client, bootstrap, host, state }) {
1016
1802
  case "digest":
1017
1803
  void runDigest(/--refresh|-r\b/.test(argument));
1018
1804
  return;
1805
+ case "activity": {
1806
+ const flag = argument.toLowerCase();
1807
+ if (flag === "on" || flag === "off") {
1808
+ const next = flag === "on";
1809
+ setActivityOn(next);
1810
+ void patchState({ activityUpdates: next });
1811
+ notice(next ? "Activity updates on \u2014 new batch activity shows up as it happens." : "Activity updates off.");
1812
+ return;
1813
+ }
1814
+ const count = Number.parseInt(argument, 10);
1815
+ void showRecentActivity(
1816
+ Number.isInteger(count) && count > 0 ? Math.min(count, ACTIVITY_LIST_MAX) : ACTIVITY_LIST_DEFAULT
1817
+ );
1818
+ return;
1819
+ }
1820
+ case "who": {
1821
+ const flag = argument.toLowerCase();
1822
+ if (flag === "on" || flag === "off") {
1823
+ const next = flag === "on";
1824
+ setPresenceOn(next);
1825
+ void patchState({ presence: next });
1826
+ notice(
1827
+ next ? "Presence on \u2014 teammates can see you're here and what you're on." : "Presence off \u2014 you're hidden, and the roster is hidden from you."
1828
+ );
1829
+ return;
1830
+ }
1831
+ if (!presenceOn) {
1832
+ notice("Presence is off. `/who on` to turn it back on.", "error");
1833
+ return;
1834
+ }
1835
+ void showPresence();
1836
+ return;
1837
+ }
1838
+ case "trending": {
1839
+ const count = Number.parseInt(argument, 10);
1840
+ void showTrending(
1841
+ Number.isInteger(count) && count > 0 ? Math.min(count, TRENDING_LIST_MAX) : TRENDING_LIST_DEFAULT
1842
+ );
1843
+ return;
1844
+ }
1845
+ case "fav":
1846
+ void runFav(argument, attached);
1847
+ return;
1848
+ case "unfav":
1849
+ void runUnfav(argument, attached);
1850
+ return;
1851
+ case "favs":
1852
+ void runFavs(argument);
1853
+ return;
1019
1854
  default:
1020
1855
  notice(`Unknown command \u201C/${command}\u201D. Type / to see them.`, "error");
1021
1856
  }
1022
1857
  },
1023
- [batch, bootstrap.teams, exit, modelId, notice, runDigest, teamId, webSearch]
1858
+ [
1859
+ batch,
1860
+ bootstrap.teams,
1861
+ exit,
1862
+ modelId,
1863
+ notice,
1864
+ runDigest,
1865
+ runFav,
1866
+ runFavs,
1867
+ runUnfav,
1868
+ showPresence,
1869
+ showRecentActivity,
1870
+ showTrending,
1871
+ presenceOn,
1872
+ teamId,
1873
+ webSearch
1874
+ ]
1024
1875
  );
1025
1876
  const submit = useCallback(() => {
1026
1877
  const raw = value.trim();
1878
+ const attached = context.filter((c) => raw.includes(`@${c.label}`));
1879
+ const company = attached.filter((c) => c.type === "company").at(-1);
1880
+ if (company) viewingRef.current = company.label;
1027
1881
  setValue("");
1028
1882
  setCursor(0);
1883
+ setContext([]);
1029
1884
  setHistoryIndex(null);
1030
1885
  setMentionQuery(null);
1031
1886
  if (!raw) return;
1032
1887
  setHistory((prev) => [...prev, raw]);
1033
1888
  if (raw.startsWith("/")) {
1034
- runCommand(raw);
1889
+ runCommand(raw, attached);
1035
1890
  return;
1036
1891
  }
1037
- void send(raw);
1038
- }, [runCommand, send, value]);
1039
- const syncMention = useCallback((text, at) => {
1040
- const match = /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, at));
1892
+ void send(raw, attached);
1893
+ }, [context, runCommand, send, value]);
1894
+ const mentionSpans = useMemo(() => findMentionSpans(value, context), [context, value]);
1895
+ const syncMention = useCallback((text, at, records) => {
1896
+ const inside = findMentionSpans(text, records).some((s) => at > s.start && at <= s.end);
1897
+ const match = inside ? null : /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, at));
1041
1898
  setMentionQuery(match ? match[1] : null);
1042
1899
  }, []);
1043
1900
  const applyEdit = useCallback(
1044
1901
  (text, at) => {
1902
+ const kept = context.filter((c) => text.includes(`@${c.label}`));
1903
+ if (kept.length !== context.length) setContext(kept);
1045
1904
  setValue(text);
1046
1905
  setCursor(at);
1047
- syncMention(text, at);
1906
+ syncMention(text, at, kept);
1048
1907
  },
1049
- [syncMention]
1908
+ [context, syncMention]
1050
1909
  );
1051
1910
  const acceptPick = useCallback(() => {
1052
1911
  if (slashQuery !== null) {
@@ -1059,7 +1918,7 @@ function App({ client, bootstrap, host, state }) {
1059
1918
  }
1060
1919
  const record = mentionRecords[selected];
1061
1920
  if (!record) return;
1062
- const before = value.slice(0, cursor).replace(/@[^\s@]*$/, "");
1921
+ const before = value.slice(0, cursor).replace(/@[^\s@]*$/, `@${record.label} `);
1063
1922
  setContext(
1064
1923
  (prev) => prev.some((c) => c.type === record.type && c.rawId === record.rawId) ? prev : [...prev, record]
1065
1924
  );
@@ -1082,6 +1941,18 @@ function App({ client, bootstrap, host, state }) {
1082
1941
  return;
1083
1942
  }
1084
1943
  if (exitArmed) setExitArmed(false);
1944
+ if (modal) {
1945
+ if (key.escape) {
1946
+ modal.resolve(null);
1947
+ } else if (key.upArrow) {
1948
+ setSelected((i) => (i - 1 + modal.rows.length) % modal.rows.length);
1949
+ } else if (key.downArrow) {
1950
+ setSelected((i) => (i + 1) % modal.rows.length);
1951
+ } else if (key.return || key.tab) {
1952
+ modal.resolve(modal.rows[selected]?.key ?? null);
1953
+ }
1954
+ return;
1955
+ }
1085
1956
  if (key.escape) {
1086
1957
  if (pickerOpen) {
1087
1958
  setMentionQuery(null);
@@ -1100,7 +1971,6 @@ function App({ client, bootstrap, host, state }) {
1100
1971
  setShowShortcuts(false);
1101
1972
  return;
1102
1973
  }
1103
- if (context.length > 0) setContext([]);
1104
1974
  return;
1105
1975
  }
1106
1976
  if (pickerOpen && pickerRows.length > 0) {
@@ -1177,14 +2047,23 @@ function App({ client, bootstrap, host, state }) {
1177
2047
  team?.name ?? "no team",
1178
2048
  batch ?? "no batch"
1179
2049
  ].join(" \xB7 ");
1180
- const hint = exitArmed ? "press ctrl-c again to exit" : busy ? "" : showShortcuts ? "esc to close" : "? for shortcuts";
1181
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1182
- /* @__PURE__ */ jsx5(Static, { items: transcript, children: (item) => /* @__PURE__ */ jsx5(TranscriptBlock, { item }, item.id) }),
1183
- digestText !== null ? /* @__PURE__ */ jsx5(DigestStream, { text: digestText }) : null,
1184
- live ? /* @__PURE__ */ jsx5(StreamingTurn, { turn: live }) : null,
1185
- live ? /* @__PURE__ */ jsx5(StatusLine, { turn: live }) : null,
1186
- showShortcuts ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: SHORTCUTS }) }) : null,
1187
- pickerOpen ? /* @__PURE__ */ jsx5(
2050
+ const hint = exitArmed ? "press ctrl-c again to exit" : modal ? "enter to choose \xB7 esc to cancel" : busy ? "" : showShortcuts ? "esc to close" : SHORTCUT_TIP;
2051
+ const stripWidth = presenceOn ? presenceStripWidth(presence) : 0;
2052
+ const budget = (process.stdout.columns || 80) - 4;
2053
+ const overflows = (h) => status.length + (h ? h.length + 3 : 0) + stripWidth > budget;
2054
+ const dropTip = hint === SHORTCUT_TIP && overflows(hint);
2055
+ const visibleHint = dropTip ? "" : hint;
2056
+ const presenceVisible = stripWidth > 0 && !overflows(visibleHint);
2057
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2058
+ /* @__PURE__ */ jsx7(Static, { items: transcript, children: (item) => /* @__PURE__ */ jsx7(TranscriptBlock, { item }, item.id) }),
2059
+ digestText !== null ? /* @__PURE__ */ jsx7(DigestStream, { text: digestText }) : null,
2060
+ live ? /* @__PURE__ */ jsx7(StreamingTurn, { turn: live }) : null,
2061
+ live ? /* @__PURE__ */ jsx7(StatusLine, { turn: live }) : null,
2062
+ showShortcuts ? /* @__PURE__ */ jsx7(Box7, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: SHORTCUTS }) }) : null,
2063
+ modal ? /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
2064
+ /* @__PURE__ */ jsx7(Box7, { paddingLeft: 1, children: /* @__PURE__ */ jsx7(Text7, { color: ACCENT, children: modal.title }) }),
2065
+ /* @__PURE__ */ jsx7(Picker, { rows: modal.rows, selected })
2066
+ ] }) : pickerOpen ? /* @__PURE__ */ jsx7(
1188
2067
  Picker,
1189
2068
  {
1190
2069
  rows: pickerRows,
@@ -1192,15 +2071,16 @@ function App({ client, bootstrap, host, state }) {
1192
2071
  status: mentionQuery !== null ? mentionLoading || mentionSearchedFor !== mentionQuery ? "searching\u2026" : `no records match \u201C${mentionQuery}\u201D` : void 0
1193
2072
  }
1194
2073
  ) : null,
1195
- /* @__PURE__ */ jsx5(
2074
+ /* @__PURE__ */ jsx7(
1196
2075
  Input,
1197
2076
  {
1198
2077
  value,
1199
2078
  cursor,
1200
- context,
2079
+ spans: mentionSpans,
1201
2080
  busy,
1202
2081
  status,
1203
- hint
2082
+ hint: visibleHint,
2083
+ right: presenceVisible ? /* @__PURE__ */ jsx7(PresenceStrip, { members: presence }) : null
1204
2084
  }
1205
2085
  )
1206
2086
  ] });
@@ -1208,45 +2088,58 @@ function App({ client, bootstrap, host, state }) {
1208
2088
  function DigestStream({ text }) {
1209
2089
  const spinner = useSpinner(true);
1210
2090
  if (!text.trim()) {
1211
- return /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
1212
- /* @__PURE__ */ jsxs4(Text5, { color: ACCENT, children: [
2091
+ return /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
2092
+ /* @__PURE__ */ jsxs6(Text7, { color: ACCENT, children: [
1213
2093
  spinner,
1214
2094
  " "
1215
2095
  ] }),
1216
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Summarizing the latest activity\u2026" })
2096
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Summarizing the latest activity\u2026" })
1217
2097
  ] });
1218
2098
  }
1219
- return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { children: text }) });
2099
+ return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { children: text }) });
1220
2100
  }
1221
2101
  function TranscriptBlock({ item }) {
1222
2102
  switch (item.kind) {
1223
2103
  case "banner":
1224
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
1225
- /* @__PURE__ */ jsx5(Logo, {}),
1226
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1227
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
2104
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", marginBottom: 1, children: [
2105
+ /* @__PURE__ */ jsx7(Logo, {}),
2106
+ /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
2107
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
1228
2108
  item.email,
1229
2109
  item.email ? " \xB7 " : "",
1230
2110
  item.host.replace(/^https?:\/\//, "")
1231
2111
  ] }),
1232
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
2112
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
1233
2113
  item.team,
1234
2114
  item.batch ? ` \xB7 ${item.batch}` : ""
1235
2115
  ] })
1236
2116
  ] })
1237
2117
  ] });
1238
2118
  case "digest":
1239
- return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { children: item.text }) });
2119
+ return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { children: item.text }) });
1240
2120
  case "user":
1241
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
1242
- /* @__PURE__ */ jsxs4(Box5, { children: [
1243
- /* @__PURE__ */ jsx5(Text5, { color: ACCENT, children: "> " }),
1244
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: item.text })
1245
- ] }),
1246
- item.context.length > 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \u23F5 ${item.context.map((c) => c.label).join(", ")}` }) : null
2121
+ return /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
2122
+ /* @__PURE__ */ jsx7(Text7, { color: ACCENT, children: "> " }),
2123
+ /* @__PURE__ */ jsx7(Text7, { children: /* @__PURE__ */ jsx7(
2124
+ HighlightedText,
2125
+ {
2126
+ value: item.text,
2127
+ cursor: null,
2128
+ spans: findMentionSpans(item.text, item.context),
2129
+ dimPlain: true
2130
+ }
2131
+ ) })
1247
2132
  ] });
2133
+ case "activity":
2134
+ return /* @__PURE__ */ jsx7(ActivityBlock, { heading: item.heading, items: item.items });
2135
+ case "presence":
2136
+ return /* @__PURE__ */ jsx7(PresenceBlock, { members: item.members });
2137
+ case "social":
2138
+ return /* @__PURE__ */ jsx7(SocialBlock, { lines: item.lines });
2139
+ case "trending":
2140
+ return /* @__PURE__ */ jsx7(TrendingBlock, { companies: item.companies, batch: item.batch, days: item.days });
1248
2141
  case "assistant":
1249
- return /* @__PURE__ */ jsx5(
2142
+ return /* @__PURE__ */ jsx7(
1250
2143
  AssistantTurn,
1251
2144
  {
1252
2145
  text: item.text,
@@ -1256,7 +2149,7 @@ function TranscriptBlock({ item }) {
1256
2149
  }
1257
2150
  );
1258
2151
  case "notice":
1259
- return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: item.tone === "error" ? "red" : void 0, dimColor: item.tone !== "error", children: item.text }) });
2152
+ return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: item.tone === "error" ? "red" : void 0, dimColor: item.tone !== "error", children: item.text }) });
1260
2153
  }
1261
2154
  }
1262
2155
  function readTurn(message) {
@@ -1319,7 +2212,7 @@ function truncateError(message) {
1319
2212
  }
1320
2213
 
1321
2214
  // src/oc.tsx
1322
- import { jsx as jsx6 } from "react/jsx-runtime";
2215
+ import { jsx as jsx8 } from "react/jsx-runtime";
1323
2216
  var USAGE = `oc \u2014 Orange Collective in your terminal
1324
2217
 
1325
2218
  Usage
@@ -1404,7 +2297,7 @@ ${USAGE}`);
1404
2297
  fail("Your account isn't on a team yet \u2014 ask an admin to add you.");
1405
2298
  }
1406
2299
  const instance = render(
1407
- /* @__PURE__ */ jsx6(App, { client, bootstrap, host, state: state ?? {} }),
2300
+ /* @__PURE__ */ jsx8(App, { client, bootstrap, host, state: state ?? {} }),
1408
2301
  // Ctrl-C is handled in App (press twice), so Ink must not exit on the first.
1409
2302
  { exitOnCtrlC: false }
1410
2303
  );