@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.
- package/README.md +19 -4
- package/dist/oc.js +1001 -108
- 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.
|
|
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 = "
|
|
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
|
|
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/
|
|
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 {
|
|
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
|
-
|
|
802
|
+
spans,
|
|
517
803
|
busy,
|
|
518
804
|
status,
|
|
519
|
-
hint
|
|
805
|
+
hint,
|
|
806
|
+
right
|
|
520
807
|
}) {
|
|
521
|
-
return /* @__PURE__ */
|
|
522
|
-
|
|
523
|
-
/* @__PURE__ */
|
|
524
|
-
/* @__PURE__ */
|
|
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__ */
|
|
535
|
-
/* @__PURE__ */
|
|
536
|
-
|
|
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
|
|
543
|
-
import { jsx as
|
|
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__ */
|
|
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__ */
|
|
559
|
-
return /* @__PURE__ */
|
|
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
|
|
564
|
-
import { jsx as
|
|
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__ */
|
|
861
|
+
return status ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: status }) }) : null;
|
|
580
862
|
}
|
|
581
|
-
return /* @__PURE__ */
|
|
863
|
+
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: visible.map((row) => {
|
|
582
864
|
const isSelected = rows[selected]?.key === row.key;
|
|
583
|
-
return /* @__PURE__ */
|
|
584
|
-
/* @__PURE__ */
|
|
585
|
-
/* @__PURE__ */
|
|
586
|
-
row.meta ? /* @__PURE__ */
|
|
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
|
|
594
|
-
import { jsx as
|
|
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__ */
|
|
608
|
-
/* @__PURE__ */
|
|
609
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1003
|
+
/* @__PURE__ */ jsx6(Text6, { bold: tool.state === "running" && live, children: tool.name })
|
|
614
1004
|
] }),
|
|
615
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
637
|
-
/* @__PURE__ */
|
|
638
|
-
thoughtSeconds != null ? /* @__PURE__ */
|
|
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__ */
|
|
644
|
-
/* @__PURE__ */
|
|
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__ */
|
|
651
|
-
/* @__PURE__ */
|
|
652
|
-
!body && reasoningTail ? /* @__PURE__ */
|
|
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
|
-
|
|
1044
|
+
truncate2(reasoningTail, (process.stdout.columns || 80) - 4)
|
|
655
1045
|
] }) : null,
|
|
656
|
-
body ? /* @__PURE__ */
|
|
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__ */
|
|
670
|
-
/* @__PURE__ */
|
|
1059
|
+
return /* @__PURE__ */ jsxs5(Box6, { children: [
|
|
1060
|
+
/* @__PURE__ */ jsxs5(Text6, { color: ACCENT, children: [
|
|
671
1061
|
spinner,
|
|
672
1062
|
" "
|
|
673
1063
|
] }),
|
|
674
|
-
/* @__PURE__ */
|
|
1064
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
675
1065
|
verb,
|
|
676
1066
|
"\u2026 "
|
|
677
1067
|
] }),
|
|
678
|
-
/* @__PURE__ */
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
|
1040
|
-
|
|
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" :
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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__ */
|
|
2074
|
+
/* @__PURE__ */ jsx7(
|
|
1196
2075
|
Input,
|
|
1197
2076
|
{
|
|
1198
2077
|
value,
|
|
1199
2078
|
cursor,
|
|
1200
|
-
|
|
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__ */
|
|
1212
|
-
/* @__PURE__ */
|
|
2091
|
+
return /* @__PURE__ */ jsxs6(Box7, { marginBottom: 1, children: [
|
|
2092
|
+
/* @__PURE__ */ jsxs6(Text7, { color: ACCENT, children: [
|
|
1213
2093
|
spinner,
|
|
1214
2094
|
" "
|
|
1215
2095
|
] }),
|
|
1216
|
-
/* @__PURE__ */
|
|
2096
|
+
/* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Summarizing the latest activity\u2026" })
|
|
1217
2097
|
] });
|
|
1218
2098
|
}
|
|
1219
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
1225
|
-
/* @__PURE__ */
|
|
1226
|
-
/* @__PURE__ */
|
|
1227
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
2119
|
+
return /* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { children: item.text }) });
|
|
1240
2120
|
case "user":
|
|
1241
|
-
return /* @__PURE__ */
|
|
1242
|
-
/* @__PURE__ */
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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__ */
|
|
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__ */
|
|
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
|
|
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__ */
|
|
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
|
);
|