@orangecollective/oc 0.2.0 → 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 (2) hide show
  1. package/dist/oc.js +350 -62
  2. 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.2.0" : "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");
@@ -406,7 +407,7 @@ var OcClient = class {
406
407
  companies: "6",
407
408
  founders: "6",
408
409
  skills: "4",
409
- sections: "batches,companies,founders,skills"
410
+ sections: "batches,companies,founders,members,skills"
410
411
  });
411
412
  if (args.teamId != null) params.set("teamId", String(args.teamId));
412
413
  const res = await this.request(`/api/search/context?${params}`, { signal: args.signal });
@@ -472,6 +473,32 @@ var OcClient = class {
472
473
  const json = await res.json();
473
474
  return json.id;
474
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
+ }
475
502
  /**
476
503
  * Transport for POST /api/chat. Sends exactly the body the web chat panel's
477
504
  * transport sends, so the route needs no CLI-specific branch.
@@ -514,6 +541,13 @@ function flattenContextResults(raw) {
514
541
  label: f.name,
515
542
  iconUrl: f.avatarUrl ?? void 0
516
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
+ })),
517
551
  ...(data.skills ?? []).map((s) => ({
518
552
  type: "skill",
519
553
  rawId: s.slug,
@@ -531,9 +565,18 @@ function buildContextPrefix(items) {
531
565
 
532
566
  // src/ui/App.tsx
533
567
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
534
- import { Box as Box6, Static, Text as Text6, useApp, useInput } from "ink";
568
+ import { Box as Box7, Static, Text as Text7, useApp, useInput } from "ink";
535
569
  import { generateId, readUIMessageStream } from "ai";
536
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
+
537
580
  // src/markdown.ts
538
581
  import { Marked } from "marked";
539
582
  import { markedTerminal } from "marked-terminal";
@@ -759,7 +802,8 @@ function Input({
759
802
  spans,
760
803
  busy,
761
804
  status,
762
- hint
805
+ hint,
806
+ right
763
807
  }) {
764
808
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
765
809
  /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? "gray" : ACCENT, paddingX: 1, children: [
@@ -767,8 +811,11 @@ function Input({
767
811
  value.length === 0 ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: PLACEHOLDER }) : /* @__PURE__ */ jsx2(Text2, { children: /* @__PURE__ */ jsx2(HighlightedText, { value, cursor, spans }) })
768
812
  ] }),
769
813
  /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 1, children: [
770
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: status }),
771
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: hint ? ` ${hint}` : "" })
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
772
819
  ] })
773
820
  ] });
774
821
  }
@@ -823,10 +870,118 @@ function Picker({
823
870
  }) });
824
871
  }
825
872
 
826
- // src/ui/Turn.tsx
827
- import { useEffect, useState } from "react";
873
+ // src/ui/Social.tsx
828
874
  import { Box as Box5, Text as Text5 } from "ink";
829
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
+
981
+ // src/ui/Turn.tsx
982
+ import { useEffect, useState } from "react";
983
+ import { Box as Box6, Text as Text6 } from "ink";
984
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
830
985
  var SPINNER = ["\u2722", "\u2733", "\u2736", "\u273B", "\u273D", "\u273B", "\u2736", "\u2733"];
831
986
  function useSpinner(active) {
832
987
  const [frame, setFrame] = useState(0);
@@ -839,15 +994,15 @@ function useSpinner(active) {
839
994
  }
840
995
  function ToolRows({ tools, live }) {
841
996
  if (tools.length === 0) return null;
842
- return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: tools.map((tool) => /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
843
- /* @__PURE__ */ jsxs4(Box5, { children: [
844
- /* @__PURE__ */ jsxs4(Text5, { 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: [
845
1000
  "\u25CF",
846
1001
  " "
847
1002
  ] }),
848
- /* @__PURE__ */ jsx5(Text5, { bold: tool.state === "running" && live, children: tool.name })
1003
+ /* @__PURE__ */ jsx6(Text6, { bold: tool.state === "running" && live, children: tool.name })
849
1004
  ] }),
850
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1005
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
851
1006
  " \u23BF ",
852
1007
  tool.state === "running" ? "running\u2026" : tool.state === "error" ? "failed" : "done"
853
1008
  ] })
@@ -855,7 +1010,7 @@ function ToolRows({ tools, live }) {
855
1010
  }
856
1011
  function Sources({ sources }) {
857
1012
  if (sources.length === 0) return null;
858
- return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: sources.map((source, index) => /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1013
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginTop: 1, children: sources.map((source, index) => /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
859
1014
  ` ${index + 1}. `,
860
1015
  source.title ? `${source.title} \u2014 ` : "",
861
1016
  source.url
@@ -868,27 +1023,27 @@ function AssistantTurn({
868
1023
  thoughtSeconds
869
1024
  }) {
870
1025
  const body = renderMarkdown(stripContextLeakage(text));
871
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
872
- /* @__PURE__ */ jsx5(ToolRows, { tools, live: false }),
873
- thoughtSeconds != null ? /* @__PURE__ */ jsxs4(Text5, { 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: [
874
1029
  "\u273B thought for ",
875
1030
  thoughtSeconds,
876
1031
  "s"
877
1032
  ] }) : null,
878
- body ? /* @__PURE__ */ jsx5(Box5, { marginTop: tools.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx5(Text5, { children: body }) }) : null,
879
- /* @__PURE__ */ jsx5(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 })
880
1035
  ] });
881
1036
  }
882
1037
  function StreamingTurn({ turn }) {
883
1038
  const body = stripContextLeakage(turn.text);
884
1039
  const reasoningTail = turn.reasoning.trim().split("\n").at(-1) ?? "";
885
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
886
- /* @__PURE__ */ jsx5(ToolRows, { tools: turn.tools, live: true }),
887
- !body && reasoningTail ? /* @__PURE__ */ jsxs4(Text5, { 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: [
888
1043
  " ",
889
- truncate(reasoningTail, (process.stdout.columns || 80) - 4)
1044
+ truncate2(reasoningTail, (process.stdout.columns || 80) - 4)
890
1045
  ] }) : null,
891
- body ? /* @__PURE__ */ jsx5(Text5, { children: body }) : null
1046
+ body ? /* @__PURE__ */ jsx6(Text6, { children: body }) : null
892
1047
  ] });
893
1048
  }
894
1049
  function StatusLine({ turn }) {
@@ -901,23 +1056,23 @@ function StatusLine({ turn }) {
901
1056
  const elapsed = Math.max(0, Math.round((now - turn.startedAt) / 1e3));
902
1057
  const running = turn.tools.find((t) => t.state === "running");
903
1058
  const verb = running ? running.name : turn.text ? "Writing" : "Thinking";
904
- return /* @__PURE__ */ jsxs4(Box5, { children: [
905
- /* @__PURE__ */ jsxs4(Text5, { color: ACCENT, children: [
1059
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
1060
+ /* @__PURE__ */ jsxs5(Text6, { color: ACCENT, children: [
906
1061
  spinner,
907
1062
  " "
908
1063
  ] }),
909
- /* @__PURE__ */ jsxs4(Text5, { children: [
1064
+ /* @__PURE__ */ jsxs5(Text6, { children: [
910
1065
  verb,
911
1066
  "\u2026 "
912
1067
  ] }),
913
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
1068
+ /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
914
1069
  "(",
915
1070
  elapsed,
916
1071
  "s \xB7 esc to interrupt)"
917
1072
  ] })
918
1073
  ] });
919
1074
  }
920
- function truncate(value, width) {
1075
+ function truncate2(value, width) {
921
1076
  if (width <= 1 || value.length <= width) return value;
922
1077
  return `${value.slice(0, Math.max(1, width - 1))}\u2026`;
923
1078
  }
@@ -932,13 +1087,16 @@ var EMPTY_TURN = {
932
1087
  };
933
1088
 
934
1089
  // src/ui/App.tsx
935
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1090
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
936
1091
  var FLUSH_INTERVAL_MS = 60;
937
1092
  var MENTION_DEBOUNCE_MS = 200;
938
1093
  var EXIT_CONFIRM_MS = 3e3;
939
1094
  var ACTIVITY_POLL_MS = 6e4;
940
1095
  var ACTIVITY_LIST_DEFAULT = 10;
941
1096
  var ACTIVITY_LIST_MAX = 30;
1097
+ var SHORTCUT_TIP = "? for shortcuts";
1098
+ var TRENDING_LIST_DEFAULT = 8;
1099
+ var TRENDING_LIST_MAX = 25;
942
1100
  var COMMANDS = [
943
1101
  { name: "model", args: "[name]", blurb: "switch model" },
944
1102
  { name: "web", blurb: "toggle web search" },
@@ -946,6 +1104,8 @@ var COMMANDS = [
946
1104
  { name: "batch", args: "[W26]", blurb: "set the batch for the digest" },
947
1105
  { name: "digest", args: "[-r]", blurb: "re-run the activity digest" },
948
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" },
949
1109
  { name: "fav", args: "<company> [in folder]", blurb: "add a company to a favorites folder" },
950
1110
  { name: "unfav", args: "<company>", blurb: "remove your favorite" },
951
1111
  { name: "favs", args: "[folder]", blurb: "list folders, or a folder's companies" },
@@ -1004,6 +1164,12 @@ function App({ client, bootstrap, host, state }) {
1004
1164
  const [activityOn, setActivityOn] = useState2(state.activityUpdates ?? true);
1005
1165
  const activityCursorRef = useRef(null);
1006
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);
1007
1173
  const [teamId, setTeamId] = useState2(
1008
1174
  state.activeTeamId ?? bootstrap.activeTeamId
1009
1175
  );
@@ -1135,6 +1301,11 @@ function App({ client, bootstrap, host, state }) {
1135
1301
  };
1136
1302
  }, [client, mentionQuery, teamId]);
1137
1303
  const flushPendingActivity = useCallback(() => {
1304
+ const lines = pendingSocialRef.current;
1305
+ if (lines.length > 0) {
1306
+ pendingSocialRef.current = [];
1307
+ push({ kind: "social", lines });
1308
+ }
1138
1309
  const items = pendingActivityRef.current;
1139
1310
  if (items.length === 0) return;
1140
1311
  pendingActivityRef.current = [];
@@ -1179,6 +1350,80 @@ function App({ client, bootstrap, host, state }) {
1179
1350
  clearInterval(timer);
1180
1351
  };
1181
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
+ );
1182
1427
  const showRecentActivity = useCallback(
1183
1428
  async (count) => {
1184
1429
  if (!batch || teamId == null) {
@@ -1572,6 +1817,31 @@ ${lines.join("\n")}`);
1572
1817
  );
1573
1818
  return;
1574
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
+ }
1575
1845
  case "fav":
1576
1846
  void runFav(argument, attached);
1577
1847
  return;
@@ -1595,7 +1865,10 @@ ${lines.join("\n")}`);
1595
1865
  runFav,
1596
1866
  runFavs,
1597
1867
  runUnfav,
1868
+ showPresence,
1598
1869
  showRecentActivity,
1870
+ showTrending,
1871
+ presenceOn,
1599
1872
  teamId,
1600
1873
  webSearch
1601
1874
  ]
@@ -1603,6 +1876,8 @@ ${lines.join("\n")}`);
1603
1876
  const submit = useCallback(() => {
1604
1877
  const raw = value.trim();
1605
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;
1606
1881
  setValue("");
1607
1882
  setCursor(0);
1608
1883
  setContext([]);
@@ -1772,17 +2047,23 @@ ${lines.join("\n")}`);
1772
2047
  team?.name ?? "no team",
1773
2048
  batch ?? "no batch"
1774
2049
  ].join(" \xB7 ");
1775
- const hint = exitArmed ? "press ctrl-c again to exit" : modal ? "enter to choose \xB7 esc to cancel" : busy ? "" : showShortcuts ? "esc to close" : "? for shortcuts";
1776
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1777
- /* @__PURE__ */ jsx6(Static, { items: transcript, children: (item) => /* @__PURE__ */ jsx6(TranscriptBlock, { item }, item.id) }),
1778
- digestText !== null ? /* @__PURE__ */ jsx6(DigestStream, { text: digestText }) : null,
1779
- live ? /* @__PURE__ */ jsx6(StreamingTurn, { turn: live }) : null,
1780
- live ? /* @__PURE__ */ jsx6(StatusLine, { turn: live }) : null,
1781
- showShortcuts ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: SHORTCUTS }) }) : null,
1782
- modal ? /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", children: [
1783
- /* @__PURE__ */ jsx6(Box6, { paddingLeft: 1, children: /* @__PURE__ */ jsx6(Text6, { color: ACCENT, children: modal.title }) }),
1784
- /* @__PURE__ */ jsx6(Picker, { rows: modal.rows, selected })
1785
- ] }) : pickerOpen ? /* @__PURE__ */ jsx6(
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(
1786
2067
  Picker,
1787
2068
  {
1788
2069
  rows: pickerRows,
@@ -1790,7 +2071,7 @@ ${lines.join("\n")}`);
1790
2071
  status: mentionQuery !== null ? mentionLoading || mentionSearchedFor !== mentionQuery ? "searching\u2026" : `no records match \u201C${mentionQuery}\u201D` : void 0
1791
2072
  }
1792
2073
  ) : null,
1793
- /* @__PURE__ */ jsx6(
2074
+ /* @__PURE__ */ jsx7(
1794
2075
  Input,
1795
2076
  {
1796
2077
  value,
@@ -1798,7 +2079,8 @@ ${lines.join("\n")}`);
1798
2079
  spans: mentionSpans,
1799
2080
  busy,
1800
2081
  status,
1801
- hint
2082
+ hint: visibleHint,
2083
+ right: presenceVisible ? /* @__PURE__ */ jsx7(PresenceStrip, { members: presence }) : null
1802
2084
  }
1803
2085
  )
1804
2086
  ] });
@@ -1806,39 +2088,39 @@ ${lines.join("\n")}`);
1806
2088
  function DigestStream({ text }) {
1807
2089
  const spinner = useSpinner(true);
1808
2090
  if (!text.trim()) {
1809
- return /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1810
- /* @__PURE__ */ jsxs5(Text6, { color: ACCENT, children: [
2091
+ return /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
2092
+ /* @__PURE__ */ jsxs6(Text7, { color: ACCENT, children: [
1811
2093
  spinner,
1812
2094
  " "
1813
2095
  ] }),
1814
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Summarizing the latest activity\u2026" })
2096
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Summarizing the latest activity\u2026" })
1815
2097
  ] });
1816
2098
  }
1817
- return /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { children: text }) });
2099
+ return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { children: text }) });
1818
2100
  }
1819
2101
  function TranscriptBlock({ item }) {
1820
2102
  switch (item.kind) {
1821
2103
  case "banner":
1822
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", marginBottom: 1, children: [
1823
- /* @__PURE__ */ jsx6(Logo, {}),
1824
- /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
1825
- /* @__PURE__ */ jsxs5(Text6, { 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: [
1826
2108
  item.email,
1827
2109
  item.email ? " \xB7 " : "",
1828
2110
  item.host.replace(/^https?:\/\//, "")
1829
2111
  ] }),
1830
- /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
2112
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
1831
2113
  item.team,
1832
2114
  item.batch ? ` \xB7 ${item.batch}` : ""
1833
2115
  ] })
1834
2116
  ] })
1835
2117
  ] });
1836
2118
  case "digest":
1837
- return /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { children: item.text }) });
2119
+ return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { children: item.text }) });
1838
2120
  case "user":
1839
- return /* @__PURE__ */ jsxs5(Box6, { marginBottom: 1, children: [
1840
- /* @__PURE__ */ jsx6(Text6, { color: ACCENT, children: "> " }),
1841
- /* @__PURE__ */ jsx6(Text6, { children: /* @__PURE__ */ jsx6(
2121
+ return /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
2122
+ /* @__PURE__ */ jsx7(Text7, { color: ACCENT, children: "> " }),
2123
+ /* @__PURE__ */ jsx7(Text7, { children: /* @__PURE__ */ jsx7(
1842
2124
  HighlightedText,
1843
2125
  {
1844
2126
  value: item.text,
@@ -1849,9 +2131,15 @@ function TranscriptBlock({ item }) {
1849
2131
  ) })
1850
2132
  ] });
1851
2133
  case "activity":
1852
- return /* @__PURE__ */ jsx6(ActivityBlock, { heading: item.heading, items: item.items });
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 });
1853
2141
  case "assistant":
1854
- return /* @__PURE__ */ jsx6(
2142
+ return /* @__PURE__ */ jsx7(
1855
2143
  AssistantTurn,
1856
2144
  {
1857
2145
  text: item.text,
@@ -1861,7 +2149,7 @@ function TranscriptBlock({ item }) {
1861
2149
  }
1862
2150
  );
1863
2151
  case "notice":
1864
- return /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsx6(Text6, { 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 }) });
1865
2153
  }
1866
2154
  }
1867
2155
  function readTurn(message) {
@@ -1924,7 +2212,7 @@ function truncateError(message) {
1924
2212
  }
1925
2213
 
1926
2214
  // src/oc.tsx
1927
- import { jsx as jsx7 } from "react/jsx-runtime";
2215
+ import { jsx as jsx8 } from "react/jsx-runtime";
1928
2216
  var USAGE = `oc \u2014 Orange Collective in your terminal
1929
2217
 
1930
2218
  Usage
@@ -2009,7 +2297,7 @@ ${USAGE}`);
2009
2297
  fail("Your account isn't on a team yet \u2014 ask an admin to add you.");
2010
2298
  }
2011
2299
  const instance = render(
2012
- /* @__PURE__ */ jsx7(App, { client, bootstrap, host, state: state ?? {} }),
2300
+ /* @__PURE__ */ jsx8(App, { client, bootstrap, host, state: state ?? {} }),
2013
2301
  // Ctrl-C is handled in App (press twice), so Ink must not exit on the first.
2014
2302
  { exitOnCtrlC: false }
2015
2303
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecollective/oc",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Orange Collective in your terminal — the YC research agent, batch digests, and company context",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.orangecollective.vc",