arp-tui 0.0.1 → 0.0.3
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 +20 -0
- package/dist/index.js +807 -107
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs as nodeParseArgs } from "util";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
|
|
7
7
|
// src/app.tsx
|
|
8
|
-
import { Box as
|
|
8
|
+
import { Box as Box8, Text as Text10, useApp, useInput, useStdin } from "ink";
|
|
9
9
|
import { useEffect, useSyncExternalStore } from "react";
|
|
10
10
|
|
|
11
11
|
// src/state/store.ts
|
|
@@ -180,7 +180,9 @@ Profiles defined: ${Object.keys(profiles).join(", ")}. Fix "profile" or add the
|
|
|
180
180
|
profile: requested,
|
|
181
181
|
profiles,
|
|
182
182
|
lastChannelId: typeof cfg["lastChannelId"] === "string" ? cfg["lastChannelId"] : void 0,
|
|
183
|
-
lastSeq: cfg["lastSeq"] && typeof cfg["lastSeq"] === "object" ? cfg["lastSeq"] : {}
|
|
183
|
+
lastSeq: cfg["lastSeq"] && typeof cfg["lastSeq"] === "object" ? cfg["lastSeq"] : {},
|
|
184
|
+
unread: cfg["unread"] && typeof cfg["unread"] === "object" ? cfg["unread"] : {},
|
|
185
|
+
lastSeen: cfg["lastSeen"] && typeof cfg["lastSeen"] === "object" ? cfg["lastSeen"] : {}
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
188
|
function resolveProfile(cfg, overrides) {
|
|
@@ -205,6 +207,61 @@ function resolveProfile(cfg, overrides) {
|
|
|
205
207
|
function saveConfig(cfg) {
|
|
206
208
|
writeFileAtomic0600(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
207
209
|
}
|
|
210
|
+
var PROFILE_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
211
|
+
function assertValidProfileName(name) {
|
|
212
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
213
|
+
throw new ConfigError(
|
|
214
|
+
`invalid profile name "${name}" \u2014 use letters, digits, dot, dash or underscore only.`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function listProfiles() {
|
|
219
|
+
const cfg = loadConfig();
|
|
220
|
+
return { active: cfg.profile, profiles: cfg.profiles };
|
|
221
|
+
}
|
|
222
|
+
function addProfile(name, raw) {
|
|
223
|
+
assertValidProfileName(name);
|
|
224
|
+
const cfg = loadConfig();
|
|
225
|
+
if (Object.hasOwn(cfg.profiles, name)) {
|
|
226
|
+
throw new ConfigError(
|
|
227
|
+
`profile "${name}" already exists (${cfg.profiles[name].relayUrl}).
|
|
228
|
+
Remove it first (arp-tui profile remove ${name}) or choose another name.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const profile = normalizeProfile(name, raw);
|
|
232
|
+
cfg.profiles[name] = profile;
|
|
233
|
+
saveConfig(cfg);
|
|
234
|
+
return profile;
|
|
235
|
+
}
|
|
236
|
+
function switchProfile(name) {
|
|
237
|
+
const cfg = loadConfig();
|
|
238
|
+
if (!Object.hasOwn(cfg.profiles, name)) {
|
|
239
|
+
throw new ConfigError(
|
|
240
|
+
`profile "${name}" does not exist. Defined: ${Object.keys(cfg.profiles).join(", ")}.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
cfg.profile = name;
|
|
244
|
+
saveConfig(cfg);
|
|
245
|
+
return cfg.profiles[name];
|
|
246
|
+
}
|
|
247
|
+
function removeProfile(name) {
|
|
248
|
+
const cfg = loadConfig();
|
|
249
|
+
if (!Object.hasOwn(cfg.profiles, name)) {
|
|
250
|
+
throw new ConfigError(
|
|
251
|
+
`profile "${name}" does not exist. Defined: ${Object.keys(cfg.profiles).join(", ")}.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (Object.keys(cfg.profiles).length === 1) {
|
|
255
|
+
throw new ConfigError(`cannot remove the only profile "${name}".`);
|
|
256
|
+
}
|
|
257
|
+
if (cfg.profile === name) {
|
|
258
|
+
throw new ConfigError(
|
|
259
|
+
`cannot remove the active profile "${name}" \u2014 switch to another first (arp-tui profile switch <name>).`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
delete cfg.profiles[name];
|
|
263
|
+
saveConfig(cfg);
|
|
264
|
+
}
|
|
208
265
|
function assertStorePerms() {
|
|
209
266
|
ensureDir0700(CONFIG_DIR);
|
|
210
267
|
const problems = [
|
|
@@ -724,8 +781,6 @@ async function logout(instance2, log = console.error) {
|
|
|
724
781
|
}
|
|
725
782
|
|
|
726
783
|
// src/api/clerkRefresh.ts
|
|
727
|
-
var DEFAULT_CLERK_FAPI = "https://generous-fish-48.clerk.accounts.dev";
|
|
728
|
-
var DEFAULT_JWT_TEMPLATE = "arp";
|
|
729
784
|
var CACHE_TTL_MS = 45e3;
|
|
730
785
|
var FAPI_TIMEOUT_MS = 1e4;
|
|
731
786
|
var ClerkRefreshError = class extends Error {
|
|
@@ -734,9 +789,9 @@ var ClerkRefreshError = class extends Error {
|
|
|
734
789
|
this.name = "ClerkRefreshError";
|
|
735
790
|
}
|
|
736
791
|
};
|
|
737
|
-
function createClerkTokenSupplier(clientToken, opts
|
|
738
|
-
const fapi =
|
|
739
|
-
const template = opts.template
|
|
792
|
+
function createClerkTokenSupplier(clientToken, opts) {
|
|
793
|
+
const fapi = opts.fapiUrl.replace(/\/$/, "");
|
|
794
|
+
const template = opts.template;
|
|
740
795
|
let cached = null;
|
|
741
796
|
let sessionId = null;
|
|
742
797
|
async function fapiFetch(path3, method) {
|
|
@@ -893,8 +948,121 @@ var RelayRest = class {
|
|
|
893
948
|
async setPresence(status) {
|
|
894
949
|
await this.req("POST", "/presence", { status });
|
|
895
950
|
}
|
|
951
|
+
/** GET /attention — cross-channel unacked attention summaries (best-effort caller) */
|
|
952
|
+
async getAttention() {
|
|
953
|
+
const data = await this.req("GET", "/attention");
|
|
954
|
+
return data.attention ?? [];
|
|
955
|
+
}
|
|
956
|
+
/** GET /notifications — human-only; newest-first, <=50 */
|
|
957
|
+
async getNotifications() {
|
|
958
|
+
const data = await this.req("GET", "/notifications");
|
|
959
|
+
return data.notifications ?? [];
|
|
960
|
+
}
|
|
961
|
+
/** POST /notifications/read-all */
|
|
962
|
+
async markAllNotificationsRead() {
|
|
963
|
+
await this.req("POST", "/notifications/read-all", {});
|
|
964
|
+
}
|
|
965
|
+
/** POST /notifications/{id}/dismiss — archives one; 404 for not-yours/nonexistent (G3) */
|
|
966
|
+
async dismissNotification(id) {
|
|
967
|
+
await this.req("POST", `/notifications/${encodeURIComponent(id)}/dismiss`, {});
|
|
968
|
+
}
|
|
896
969
|
};
|
|
897
970
|
|
|
971
|
+
// src/state/activityFold.ts
|
|
972
|
+
function eventTs(e) {
|
|
973
|
+
return typeof e.ts === "number" && Number.isFinite(e.ts) ? e.ts : 0;
|
|
974
|
+
}
|
|
975
|
+
function supersedes(nextTs, nextOrder, prevTs, prevOrder) {
|
|
976
|
+
if (nextTs !== prevTs) return nextTs > prevTs;
|
|
977
|
+
return nextOrder >= prevOrder;
|
|
978
|
+
}
|
|
979
|
+
function foldActivityEvents(events) {
|
|
980
|
+
const turns = /* @__PURE__ */ new Map();
|
|
981
|
+
events.forEach((e, idx) => {
|
|
982
|
+
if (!e.turnId || !e.toolCallId) return;
|
|
983
|
+
let turn = turns.get(e.turnId);
|
|
984
|
+
if (!turn) {
|
|
985
|
+
turn = { order: [], calls: /* @__PURE__ */ new Map() };
|
|
986
|
+
turns.set(e.turnId, turn);
|
|
987
|
+
}
|
|
988
|
+
const ts = eventTs(e);
|
|
989
|
+
const existing = turn.calls.get(e.toolCallId);
|
|
990
|
+
if (!existing) {
|
|
991
|
+
turn.order.push(e.toolCallId);
|
|
992
|
+
turn.calls.set(e.toolCallId, {
|
|
993
|
+
toolCallId: e.toolCallId,
|
|
994
|
+
turnId: e.turnId,
|
|
995
|
+
agentId: e.agentId,
|
|
996
|
+
kind: e.kind,
|
|
997
|
+
title: e.title,
|
|
998
|
+
status: e.status,
|
|
999
|
+
locations: e.locations,
|
|
1000
|
+
ts,
|
|
1001
|
+
_winTs: ts,
|
|
1002
|
+
_winOrder: idx
|
|
1003
|
+
});
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (supersedes(ts, idx, existing._winTs, existing._winOrder)) {
|
|
1007
|
+
if (e.kind !== void 0) existing.kind = e.kind;
|
|
1008
|
+
if (e.title !== void 0) existing.title = e.title;
|
|
1009
|
+
if (e.status !== void 0) existing.status = e.status;
|
|
1010
|
+
if (e.locations !== void 0) existing.locations = e.locations;
|
|
1011
|
+
if (e.agentId) existing.agentId = e.agentId;
|
|
1012
|
+
existing.ts = ts;
|
|
1013
|
+
existing._winTs = ts;
|
|
1014
|
+
existing._winOrder = idx;
|
|
1015
|
+
} else {
|
|
1016
|
+
if (existing.kind === void 0 && e.kind !== void 0) existing.kind = e.kind;
|
|
1017
|
+
if (existing.title === void 0 && e.title !== void 0) existing.title = e.title;
|
|
1018
|
+
if (existing.status === void 0 && e.status !== void 0) existing.status = e.status;
|
|
1019
|
+
if (existing.locations === void 0 && e.locations !== void 0) existing.locations = e.locations;
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
const out = /* @__PURE__ */ new Map();
|
|
1023
|
+
for (const [turnId, turn] of turns) {
|
|
1024
|
+
out.set(
|
|
1025
|
+
turnId,
|
|
1026
|
+
turn.order.map((tcId) => {
|
|
1027
|
+
const c = turn.calls.get(tcId);
|
|
1028
|
+
const { _winTs, _winOrder, ...pub } = c;
|
|
1029
|
+
void _winTs;
|
|
1030
|
+
void _winOrder;
|
|
1031
|
+
return pub;
|
|
1032
|
+
})
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
return out;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/state/priority.ts
|
|
1039
|
+
function channelPriority(cid, p) {
|
|
1040
|
+
const level = p.attention[cid]?.highestLevel;
|
|
1041
|
+
if (level === "critical") return 0;
|
|
1042
|
+
if (level === "action_required") return 1;
|
|
1043
|
+
if (level === "info") return 2;
|
|
1044
|
+
if ((p.unread[cid] ?? 0) > 0) return 3;
|
|
1045
|
+
return 4;
|
|
1046
|
+
}
|
|
1047
|
+
function sortChannelsByPriority(channels, p) {
|
|
1048
|
+
return channels.map((c, i) => ({ c, pri: channelPriority(c.id, p), i })).sort((a, b) => a.pri - b.pri || a.i - b.i).map((x) => x.c);
|
|
1049
|
+
}
|
|
1050
|
+
function nextActiveChannel(channels, activeId, p) {
|
|
1051
|
+
const ordered = sortChannelsByPriority(
|
|
1052
|
+
channels.filter((c) => channelPriority(c.id, p) < 4),
|
|
1053
|
+
p
|
|
1054
|
+
);
|
|
1055
|
+
if (ordered.length === 0) return null;
|
|
1056
|
+
const idx = ordered.findIndex((c) => c.id === activeId);
|
|
1057
|
+
if (idx === -1) {
|
|
1058
|
+
return ordered[0] ?? null;
|
|
1059
|
+
}
|
|
1060
|
+
const next = ordered[(idx + 1) % ordered.length];
|
|
1061
|
+
if (!next) return null;
|
|
1062
|
+
if (next.id === activeId) return null;
|
|
1063
|
+
return next;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
898
1066
|
// src/api/ws.ts
|
|
899
1067
|
import { appendFileSync } from "fs";
|
|
900
1068
|
import WebSocket from "ws";
|
|
@@ -1134,6 +1302,10 @@ var IDLE_CHECK_MS = 3e4;
|
|
|
1134
1302
|
var TICK_MS = 1e4;
|
|
1135
1303
|
var ACTIVITY_AUTOCLEAR_MS = 6e4;
|
|
1136
1304
|
var RENDER_THROTTLE_MS = 250;
|
|
1305
|
+
var MAX_ACTIVITY_TURNS = 8;
|
|
1306
|
+
var MAX_TOOL_CALLS_PER_TURN = 50;
|
|
1307
|
+
var MAX_RAW_EVENTS_PER_TURN = 200;
|
|
1308
|
+
var ACTIVITY_SETTLE_MS = 4e3;
|
|
1137
1309
|
function createStore(opts) {
|
|
1138
1310
|
const instance2 = opts.instance;
|
|
1139
1311
|
const relayUrl = instance2.relayUrl;
|
|
@@ -1166,7 +1338,16 @@ function createStore(opts) {
|
|
|
1166
1338
|
// "on" only once it actually mints (getToken flips it)
|
|
1167
1339
|
clerkRefresh: creds?.clerkClientToken && creds.tokenType !== "oauth" ? "on" : "off",
|
|
1168
1340
|
tokenPrompt: "",
|
|
1169
|
-
tokenPromptReason: null
|
|
1341
|
+
tokenPromptReason: null,
|
|
1342
|
+
unread: { ...config.unread ?? {} },
|
|
1343
|
+
unreadCapNotice: null,
|
|
1344
|
+
attention: {},
|
|
1345
|
+
notifications: [],
|
|
1346
|
+
notifSelected: 0,
|
|
1347
|
+
activityByTurn: {},
|
|
1348
|
+
plansByTurn: {},
|
|
1349
|
+
activityPinned: false,
|
|
1350
|
+
activityIdleSince: null
|
|
1170
1351
|
};
|
|
1171
1352
|
let lastNotifyAt = 0;
|
|
1172
1353
|
let notifyTimer = null;
|
|
@@ -1345,6 +1526,75 @@ function createStore(opts) {
|
|
|
1345
1526
|
});
|
|
1346
1527
|
return fresh.length;
|
|
1347
1528
|
}
|
|
1529
|
+
const WS_SUB_CAP = 256;
|
|
1530
|
+
function bumpUnread(channelId) {
|
|
1531
|
+
if (channelId === state.activeChannelId) return;
|
|
1532
|
+
const next = (state.unread[channelId] ?? 0) + 1;
|
|
1533
|
+
const unread = { ...state.unread, [channelId]: next };
|
|
1534
|
+
config = { ...config, unread };
|
|
1535
|
+
saveConfigSoon();
|
|
1536
|
+
setThrottled({ unread });
|
|
1537
|
+
}
|
|
1538
|
+
function clearUnread(channelId) {
|
|
1539
|
+
const lastSeen = { ...config.lastSeen ?? {}, [channelId]: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1540
|
+
const unread = { ...state.unread };
|
|
1541
|
+
delete unread[channelId];
|
|
1542
|
+
config = { ...config, unread, lastSeen };
|
|
1543
|
+
saveConfigSoon();
|
|
1544
|
+
set({ unread });
|
|
1545
|
+
}
|
|
1546
|
+
const ATTENTION_POLL_MS = 3e4;
|
|
1547
|
+
const NOTIF_POLL_MS = 6e4;
|
|
1548
|
+
let attentionTimer = null;
|
|
1549
|
+
let notifTimer = null;
|
|
1550
|
+
async function pollAttention() {
|
|
1551
|
+
try {
|
|
1552
|
+
const list = await rest.getAttention();
|
|
1553
|
+
const map = {};
|
|
1554
|
+
for (const a of list) map[a.channelId] = a;
|
|
1555
|
+
setThrottled({ attention: map });
|
|
1556
|
+
} catch {
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async function pollNotifications() {
|
|
1560
|
+
try {
|
|
1561
|
+
setThrottled({ notifications: await rest.getNotifications() });
|
|
1562
|
+
} catch {
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function dismissNotification(id) {
|
|
1566
|
+
const prev = state.notifications;
|
|
1567
|
+
const prevSel = state.notifSelected;
|
|
1568
|
+
const next = prev.filter((n) => n.id !== id);
|
|
1569
|
+
const clampedSel = Math.max(0, Math.min(prevSel, next.length - 1));
|
|
1570
|
+
setThrottled({ notifications: next, notifSelected: clampedSel });
|
|
1571
|
+
try {
|
|
1572
|
+
await rest.dismissNotification(id);
|
|
1573
|
+
} catch {
|
|
1574
|
+
setThrottled({ notifications: prev, notifSelected: prevSel });
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
async function markAllNotificationsRead() {
|
|
1578
|
+
const stamped = state.notifications.map((n) => ({
|
|
1579
|
+
...n,
|
|
1580
|
+
readAt: n.readAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1581
|
+
}));
|
|
1582
|
+
setThrottled({ notifications: stamped });
|
|
1583
|
+
try {
|
|
1584
|
+
await rest.markAllNotificationsRead();
|
|
1585
|
+
} catch {
|
|
1586
|
+
void pollNotifications();
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function moveNotifSelection(delta) {
|
|
1590
|
+
const len = state.notifications.length;
|
|
1591
|
+
if (len === 0) {
|
|
1592
|
+
set({ notifSelected: 0 });
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const next = Math.max(0, Math.min(state.notifSelected + delta, len - 1));
|
|
1596
|
+
set({ notifSelected: next });
|
|
1597
|
+
}
|
|
1348
1598
|
function seedPresence(members) {
|
|
1349
1599
|
const presence = {};
|
|
1350
1600
|
for (const m of members) presence[m.id] = m.status === "online" ? "active" : m.status;
|
|
@@ -1416,6 +1666,42 @@ function createStore(opts) {
|
|
|
1416
1666
|
scheduleActivitySweep();
|
|
1417
1667
|
}
|
|
1418
1668
|
}
|
|
1669
|
+
let rawActivityByTurn = /* @__PURE__ */ new Map();
|
|
1670
|
+
let activitySettleTimer = null;
|
|
1671
|
+
function activityIsLive() {
|
|
1672
|
+
for (const rows of Object.values(state.activityByTurn))
|
|
1673
|
+
for (const r of rows) if (r.status === "pending" || r.status === "in_progress") return true;
|
|
1674
|
+
for (const entries of Object.values(state.plansByTurn))
|
|
1675
|
+
for (const e of entries) if (e.status !== "completed") return true;
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
function evictOldestTurns(rec) {
|
|
1679
|
+
const keys = Object.keys(rec);
|
|
1680
|
+
if (keys.length <= MAX_ACTIVITY_TURNS) return rec;
|
|
1681
|
+
const drop = keys.slice(0, keys.length - MAX_ACTIVITY_TURNS);
|
|
1682
|
+
const next = { ...rec };
|
|
1683
|
+
for (const k of drop) delete next[k];
|
|
1684
|
+
return next;
|
|
1685
|
+
}
|
|
1686
|
+
function reconcileActivitySettle() {
|
|
1687
|
+
if (activityIsLive()) {
|
|
1688
|
+
if (activitySettleTimer) {
|
|
1689
|
+
clearTimeout(activitySettleTimer);
|
|
1690
|
+
activitySettleTimer = null;
|
|
1691
|
+
}
|
|
1692
|
+
if (state.activityIdleSince !== null) setThrottled({ activityIdleSince: null });
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (state.activityIdleSince === null) setThrottled({ activityIdleSince: Date.now() });
|
|
1696
|
+
if (activitySettleTimer) return;
|
|
1697
|
+
activitySettleTimer = setTimeout(() => {
|
|
1698
|
+
activitySettleTimer = null;
|
|
1699
|
+
setThrottled({});
|
|
1700
|
+
}, ACTIVITY_SETTLE_MS);
|
|
1701
|
+
}
|
|
1702
|
+
function togglePin() {
|
|
1703
|
+
set({ activityPinned: !state.activityPinned });
|
|
1704
|
+
}
|
|
1419
1705
|
let countdownTimer = null;
|
|
1420
1706
|
function syncCountdown(s) {
|
|
1421
1707
|
if (s === "reconnecting" && !countdownTimer) {
|
|
@@ -1438,6 +1724,8 @@ function createStore(opts) {
|
|
|
1438
1724
|
if (isReconnect) {
|
|
1439
1725
|
pendingGapCursor = config.lastSeq?.[state.activeChannelId ?? ""] ?? null;
|
|
1440
1726
|
void gapFill();
|
|
1727
|
+
void pollAttention();
|
|
1728
|
+
void pollNotifications();
|
|
1441
1729
|
}
|
|
1442
1730
|
});
|
|
1443
1731
|
ws.onFrame((frame) => {
|
|
@@ -1459,8 +1747,12 @@ function createStore(opts) {
|
|
|
1459
1747
|
appendMessages(frame.channelId, frame.messages ?? []);
|
|
1460
1748
|
return;
|
|
1461
1749
|
}
|
|
1462
|
-
case "new_message":
|
|
1463
|
-
|
|
1750
|
+
case "new_message":
|
|
1751
|
+
case "flow_message": {
|
|
1752
|
+
if (frame.channelId !== state.activeChannelId) {
|
|
1753
|
+
bumpUnread(frame.channelId);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1464
1756
|
appendMessages(frame.channelId, [frame.message]);
|
|
1465
1757
|
const senderId = frame.message.agentId;
|
|
1466
1758
|
removeActivity(
|
|
@@ -1500,13 +1792,53 @@ function createStore(opts) {
|
|
|
1500
1792
|
return;
|
|
1501
1793
|
}
|
|
1502
1794
|
case "subscribe_error": {
|
|
1503
|
-
|
|
1795
|
+
if (frame.error === "limit") {
|
|
1796
|
+
set({ unreadCapNotice: "hit the channel-subscription cap \u2014 some channels are attention-only" });
|
|
1797
|
+
} else {
|
|
1798
|
+
set({ error: `subscribe failed for ${frame.channelId}: ${frame.error}` });
|
|
1799
|
+
}
|
|
1504
1800
|
return;
|
|
1505
1801
|
}
|
|
1506
1802
|
case "channel_archived": {
|
|
1507
1803
|
pushDivider("channel archived");
|
|
1508
1804
|
return;
|
|
1509
1805
|
}
|
|
1806
|
+
case "activity_event": {
|
|
1807
|
+
if (frame.channelId !== state.activeChannelId) return;
|
|
1808
|
+
if (!frame.turnId || !frame.toolCallId) return;
|
|
1809
|
+
const ev = {
|
|
1810
|
+
turnId: frame.turnId,
|
|
1811
|
+
toolCallId: frame.toolCallId,
|
|
1812
|
+
agentId: frame.agentId,
|
|
1813
|
+
kind: frame.kind,
|
|
1814
|
+
title: frame.title,
|
|
1815
|
+
status: frame.status,
|
|
1816
|
+
locations: frame.locations,
|
|
1817
|
+
ts: frame.ts
|
|
1818
|
+
};
|
|
1819
|
+
const list = [...rawActivityByTurn.get(frame.turnId) ?? [], ev];
|
|
1820
|
+
if (list.length > MAX_RAW_EVENTS_PER_TURN) list.splice(0, list.length - MAX_RAW_EVENTS_PER_TURN);
|
|
1821
|
+
rawActivityByTurn.set(frame.turnId, list);
|
|
1822
|
+
while (rawActivityByTurn.size > MAX_ACTIVITY_TURNS) {
|
|
1823
|
+
const oldest = rawActivityByTurn.keys().next().value;
|
|
1824
|
+
rawActivityByTurn.delete(oldest);
|
|
1825
|
+
}
|
|
1826
|
+
const foldedMap = foldActivityEvents(Array.from(rawActivityByTurn.values()).flat());
|
|
1827
|
+
let activityByTurn = {};
|
|
1828
|
+
for (const [tid, rows] of foldedMap) activityByTurn[tid] = rows.slice(0, MAX_TOOL_CALLS_PER_TURN);
|
|
1829
|
+
activityByTurn = evictOldestTurns(activityByTurn);
|
|
1830
|
+
setThrottled({ activityByTurn });
|
|
1831
|
+
reconcileActivitySettle();
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
case "plan_update": {
|
|
1835
|
+
if (frame.channelId !== state.activeChannelId) return;
|
|
1836
|
+
if (!frame.turnId) return;
|
|
1837
|
+
const plansByTurn = evictOldestTurns({ ...state.plansByTurn, [frame.turnId]: frame.entries ?? [] });
|
|
1838
|
+
setThrottled({ plansByTurn });
|
|
1839
|
+
reconcileActivitySettle();
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1510
1842
|
default:
|
|
1511
1843
|
return;
|
|
1512
1844
|
}
|
|
@@ -1624,7 +1956,19 @@ function createStore(opts) {
|
|
|
1624
1956
|
}
|
|
1625
1957
|
initialized = true;
|
|
1626
1958
|
set({ channels });
|
|
1959
|
+
const toSub = channels.slice(0, WS_SUB_CAP);
|
|
1960
|
+
for (const c of toSub) ws.subscribe(c.id);
|
|
1961
|
+
if (channels.length > WS_SUB_CAP)
|
|
1962
|
+
set({ unreadCapNotice: `unread tracked for ${WS_SUB_CAP} of ${channels.length} channels (attention still covers the rest)` });
|
|
1627
1963
|
ws.connect();
|
|
1964
|
+
void pollAttention();
|
|
1965
|
+
void pollNotifications();
|
|
1966
|
+
if (!attentionTimer) {
|
|
1967
|
+
attentionTimer = setInterval(() => void pollAttention(), ATTENTION_POLL_MS);
|
|
1968
|
+
}
|
|
1969
|
+
if (!notifTimer) {
|
|
1970
|
+
notifTimer = setInterval(() => void pollNotifications(), NOTIF_POLL_MS);
|
|
1971
|
+
}
|
|
1628
1972
|
reportPresence("active");
|
|
1629
1973
|
if (!idleTimer) {
|
|
1630
1974
|
idleTimer = setInterval(() => {
|
|
@@ -1645,7 +1989,6 @@ function createStore(opts) {
|
|
|
1645
1989
|
set({ mode: "normal" });
|
|
1646
1990
|
return;
|
|
1647
1991
|
}
|
|
1648
|
-
if (state.activeChannelId) ws.unsubscribe(state.activeChannelId);
|
|
1649
1992
|
const ch = state.channels.find((c) => c.id === channelId);
|
|
1650
1993
|
const members = ch?.members ?? [];
|
|
1651
1994
|
pushDivider(`${channelSigil(ch?.kind)}${ch?.name ?? channelId}`);
|
|
@@ -1657,8 +2000,17 @@ function createStore(opts) {
|
|
|
1657
2000
|
mode: "normal",
|
|
1658
2001
|
switcherFilter: "",
|
|
1659
2002
|
lastSeq: config.lastSeq?.[channelId] ?? null,
|
|
1660
|
-
error: null
|
|
2003
|
+
error: null,
|
|
2004
|
+
activityByTurn: {},
|
|
2005
|
+
plansByTurn: {},
|
|
2006
|
+
activityIdleSince: null
|
|
1661
2007
|
});
|
|
2008
|
+
rawActivityByTurn = /* @__PURE__ */ new Map();
|
|
2009
|
+
if (activitySettleTimer) {
|
|
2010
|
+
clearTimeout(activitySettleTimer);
|
|
2011
|
+
activitySettleTimer = null;
|
|
2012
|
+
}
|
|
2013
|
+
clearUnread(channelId);
|
|
1662
2014
|
ws.subscribe(channelId);
|
|
1663
2015
|
config = { ...config, lastChannelId: channelId };
|
|
1664
2016
|
saveConfigSoon();
|
|
@@ -1693,7 +2045,19 @@ function createStore(opts) {
|
|
|
1693
2045
|
if (idleTimer) clearInterval(idleTimer);
|
|
1694
2046
|
if (tickTimer) clearInterval(tickTimer);
|
|
1695
2047
|
if (activitySweep) clearTimeout(activitySweep);
|
|
2048
|
+
if (activitySettleTimer) {
|
|
2049
|
+
clearTimeout(activitySettleTimer);
|
|
2050
|
+
activitySettleTimer = null;
|
|
2051
|
+
}
|
|
1696
2052
|
if (countdownTimer) clearInterval(countdownTimer);
|
|
2053
|
+
if (attentionTimer) {
|
|
2054
|
+
clearInterval(attentionTimer);
|
|
2055
|
+
attentionTimer = null;
|
|
2056
|
+
}
|
|
2057
|
+
if (notifTimer) {
|
|
2058
|
+
clearInterval(notifTimer);
|
|
2059
|
+
notifTimer = null;
|
|
2060
|
+
}
|
|
1697
2061
|
if (notifyTimer) {
|
|
1698
2062
|
clearTimeout(notifyTimer);
|
|
1699
2063
|
notifyTimer = null;
|
|
@@ -1718,6 +2082,15 @@ function createStore(opts) {
|
|
|
1718
2082
|
sendDraft: () => void sendDraft(),
|
|
1719
2083
|
reconnectNow: () => ws.reconnectNow(),
|
|
1720
2084
|
noteUserActivity,
|
|
2085
|
+
dismissNotification: (id) => void dismissNotification(id),
|
|
2086
|
+
markAllNotificationsRead: () => void markAllNotificationsRead(),
|
|
2087
|
+
moveNotifSelection,
|
|
2088
|
+
jumpNextActive: () => {
|
|
2089
|
+
const s = state;
|
|
2090
|
+
const target = nextActiveChannel(s.channels, s.activeChannelId, { attention: s.attention, unread: s.unread });
|
|
2091
|
+
if (target) openChannel(target.id);
|
|
2092
|
+
},
|
|
2093
|
+
toggleActivityPin: () => togglePin(),
|
|
1721
2094
|
quit
|
|
1722
2095
|
};
|
|
1723
2096
|
}
|
|
@@ -1755,79 +2128,206 @@ function ActivityLine({ activities }) {
|
|
|
1755
2128
|
] });
|
|
1756
2129
|
}
|
|
1757
2130
|
|
|
1758
|
-
// src/ui/
|
|
2131
|
+
// src/ui/ActivitySurface.tsx
|
|
1759
2132
|
import { Box, Text as Text2 } from "ink";
|
|
1760
2133
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2134
|
+
function statusGlyph(status) {
|
|
2135
|
+
switch (status) {
|
|
2136
|
+
case "completed":
|
|
2137
|
+
return "\u2713";
|
|
2138
|
+
case "failed":
|
|
2139
|
+
return "\u2717";
|
|
2140
|
+
case "in_progress":
|
|
2141
|
+
return "\u22EF";
|
|
2142
|
+
case "pending":
|
|
2143
|
+
return "\u25CB";
|
|
2144
|
+
default:
|
|
2145
|
+
return "\xB7";
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function kindGlyph(kind) {
|
|
2149
|
+
switch (kind) {
|
|
2150
|
+
case "read":
|
|
2151
|
+
return "r";
|
|
2152
|
+
case "edit":
|
|
2153
|
+
return "e";
|
|
2154
|
+
case "write":
|
|
2155
|
+
return "w";
|
|
2156
|
+
case "run":
|
|
2157
|
+
return "\u25B6";
|
|
2158
|
+
case "search":
|
|
2159
|
+
return "s";
|
|
2160
|
+
case "fetch":
|
|
2161
|
+
return "f";
|
|
2162
|
+
case "task":
|
|
2163
|
+
return "t";
|
|
2164
|
+
default:
|
|
2165
|
+
return "\xB7";
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
function planGlyph(status) {
|
|
2169
|
+
switch (status) {
|
|
2170
|
+
case "completed":
|
|
2171
|
+
return "[x]";
|
|
2172
|
+
case "in_progress":
|
|
2173
|
+
return "[~]";
|
|
2174
|
+
default:
|
|
2175
|
+
return "[ ]";
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
function fallbackTitle(call) {
|
|
2179
|
+
if (call.title) return call.title;
|
|
2180
|
+
if (call.kind) return call.kind.charAt(0).toUpperCase() + call.kind.slice(1);
|
|
2181
|
+
return "Tool call";
|
|
2182
|
+
}
|
|
2183
|
+
function ActivitySurface(props) {
|
|
2184
|
+
const { toolCalls, plan, visible, pinned } = props;
|
|
2185
|
+
if (!visible) return null;
|
|
2186
|
+
if (toolCalls.length === 0 && plan.length === 0) {
|
|
2187
|
+
return /* @__PURE__ */ jsx2(Box, { paddingLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no activity yet" }) });
|
|
2188
|
+
}
|
|
2189
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingLeft: 1, children: [
|
|
2190
|
+
toolCalls.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
2191
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "tool calls \xB7 only you can see this" }),
|
|
2192
|
+
toolCalls.map((call) => /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
2193
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2194
|
+
statusGlyph(call.status),
|
|
2195
|
+
" ",
|
|
2196
|
+
kindGlyph(call.kind),
|
|
2197
|
+
" ",
|
|
2198
|
+
sanitizeLabel(fallbackTitle(call), 48)
|
|
2199
|
+
] }),
|
|
2200
|
+
pinned && call.locations && call.locations.length > 0 && /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: 2, children: call.locations.map((loc, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2201
|
+
sanitizeLabel(loc.path, 60),
|
|
2202
|
+
loc.line != null ? `:${loc.line}` : ""
|
|
2203
|
+
] }, i)) })
|
|
2204
|
+
] }, call.toolCallId))
|
|
2205
|
+
] }),
|
|
2206
|
+
plan.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
2207
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "plan \xB7 shared with the channel" }),
|
|
2208
|
+
plan.map((entry, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: entry.status === "completed", children: [
|
|
2209
|
+
planGlyph(entry.status),
|
|
2210
|
+
" ",
|
|
2211
|
+
sanitizeForTty(entry.content)
|
|
2212
|
+
] }, i))
|
|
2213
|
+
] })
|
|
2214
|
+
] });
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// src/ui/ChannelSwitcher.tsx
|
|
2218
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
2219
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1761
2220
|
function channelLabel(ch) {
|
|
1762
2221
|
return `${channelSigil(ch.kind)}${sanitizeLabel(ch.name)}`;
|
|
1763
2222
|
}
|
|
2223
|
+
function ChannelBadge({
|
|
2224
|
+
channelId,
|
|
2225
|
+
activeChannelId,
|
|
2226
|
+
attention,
|
|
2227
|
+
unread
|
|
2228
|
+
}) {
|
|
2229
|
+
if (channelId === activeChannelId) return null;
|
|
2230
|
+
const att = attention[channelId];
|
|
2231
|
+
if (att) {
|
|
2232
|
+
const color = att.highestLevel === "critical" ? "red" : att.highestLevel === "action_required" ? "yellow" : "cyan";
|
|
2233
|
+
const sigil = att.highestLevel === "critical" ? "!" : att.highestLevel === "action_required" ? "\u25B2" : "\u2022";
|
|
2234
|
+
return /* @__PURE__ */ jsxs3(Text3, { color, children: [
|
|
2235
|
+
" ",
|
|
2236
|
+
"[",
|
|
2237
|
+
sigil,
|
|
2238
|
+
att.totalCount,
|
|
2239
|
+
"]"
|
|
2240
|
+
] });
|
|
2241
|
+
}
|
|
2242
|
+
const count = unread[channelId] ?? 0;
|
|
2243
|
+
if (count > 0) {
|
|
2244
|
+
return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2245
|
+
" (",
|
|
2246
|
+
count,
|
|
2247
|
+
")"
|
|
2248
|
+
] });
|
|
2249
|
+
}
|
|
2250
|
+
return null;
|
|
2251
|
+
}
|
|
1764
2252
|
function ChannelSwitcher({
|
|
1765
2253
|
channels,
|
|
1766
2254
|
activeChannelId,
|
|
1767
|
-
filter
|
|
2255
|
+
filter,
|
|
2256
|
+
attention,
|
|
2257
|
+
unread
|
|
1768
2258
|
}) {
|
|
1769
2259
|
const filtered = filterChannels(channels, filter);
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1773
|
-
|
|
2260
|
+
const sorted = sortChannelsByPriority(filtered, { attention, unread });
|
|
2261
|
+
const visible = sorted.slice(0, 9);
|
|
2262
|
+
const hidden = sorted.length - visible.length;
|
|
2263
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2264
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
|
|
1774
2265
|
"channels",
|
|
1775
2266
|
" ",
|
|
1776
|
-
/* @__PURE__ */
|
|
2267
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1777
2268
|
"(",
|
|
1778
|
-
|
|
2269
|
+
sorted.length,
|
|
1779
2270
|
"/",
|
|
1780
2271
|
channels.length,
|
|
1781
2272
|
")"
|
|
1782
2273
|
] })
|
|
1783
2274
|
] }),
|
|
1784
|
-
/* @__PURE__ */
|
|
1785
|
-
/* @__PURE__ */
|
|
1786
|
-
/* @__PURE__ */
|
|
1787
|
-
/* @__PURE__ */
|
|
2275
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2276
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "filter: " }),
|
|
2277
|
+
/* @__PURE__ */ jsx3(Text3, { children: sanitizeLabel(filter, 40) }),
|
|
2278
|
+
/* @__PURE__ */ jsx3(Text3, { inverse: true, children: " " })
|
|
1788
2279
|
] }),
|
|
1789
|
-
visible.map((ch, i) => /* @__PURE__ */
|
|
1790
|
-
/* @__PURE__ */
|
|
2280
|
+
visible.map((ch, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2281
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
|
|
1791
2282
|
i + 1,
|
|
1792
2283
|
" "
|
|
1793
2284
|
] }),
|
|
1794
|
-
/* @__PURE__ */
|
|
1795
|
-
/* @__PURE__ */
|
|
2285
|
+
/* @__PURE__ */ jsx3(Text3, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
|
|
2286
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1796
2287
|
" \xB7 ",
|
|
1797
2288
|
ch.members.length,
|
|
1798
2289
|
" members"
|
|
1799
|
-
] })
|
|
2290
|
+
] }),
|
|
2291
|
+
/* @__PURE__ */ jsx3(
|
|
2292
|
+
ChannelBadge,
|
|
2293
|
+
{
|
|
2294
|
+
channelId: ch.id,
|
|
2295
|
+
activeChannelId,
|
|
2296
|
+
attention,
|
|
2297
|
+
unread
|
|
2298
|
+
}
|
|
2299
|
+
)
|
|
1800
2300
|
] }, ch.id)),
|
|
1801
|
-
hidden > 0 ? /* @__PURE__ */
|
|
2301
|
+
hidden > 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1802
2302
|
"\u2026",
|
|
1803
2303
|
hidden,
|
|
1804
2304
|
" more \u2014 keep typing to narrow"
|
|
1805
2305
|
] }) : null,
|
|
1806
|
-
|
|
2306
|
+
sorted.length === 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
1807
2307
|
'no channels match "',
|
|
1808
2308
|
sanitizeLabel(filter, 20),
|
|
1809
2309
|
'"'
|
|
1810
2310
|
] }) : null,
|
|
1811
|
-
/* @__PURE__ */
|
|
2311
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: filter.length === 0 ? "type to filter \xB7 1\u20139 quick-select \xB7 n jump \xB7 Esc cancel" : "keep typing to narrow \xB7 Enter opens top match \xB7 Esc cancel" })
|
|
1812
2312
|
] });
|
|
1813
2313
|
}
|
|
1814
2314
|
|
|
1815
2315
|
// src/ui/Composer.tsx
|
|
1816
|
-
import { Box as
|
|
1817
|
-
import { jsx as
|
|
2316
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
2317
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1818
2318
|
function Composer({ draft, focused }) {
|
|
1819
2319
|
const width = Math.max((process.stdout.columns ?? 80) - 8, 20);
|
|
1820
2320
|
const visible = draft.length > width ? "\u2026" + draft.slice(-(width - 1)) : draft;
|
|
1821
|
-
return /* @__PURE__ */
|
|
1822
|
-
/* @__PURE__ */
|
|
1823
|
-
draft.length === 0 && !focused ? /* @__PURE__ */
|
|
1824
|
-
focused ? /* @__PURE__ */
|
|
2321
|
+
return /* @__PURE__ */ jsx4(Box3, { borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, width: "100%", children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate", children: [
|
|
2322
|
+
/* @__PURE__ */ jsx4(Text4, { color: focused ? "cyan" : "gray", children: "> " }),
|
|
2323
|
+
draft.length === 0 && !focused ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "i to compose" }) : /* @__PURE__ */ jsx4(Text4, { children: visible }),
|
|
2324
|
+
focused ? /* @__PURE__ */ jsx4(Text4, { inverse: true, children: " " }) : null
|
|
1825
2325
|
] }) });
|
|
1826
2326
|
}
|
|
1827
2327
|
|
|
1828
2328
|
// src/ui/Header.tsx
|
|
1829
|
-
import { Box as
|
|
1830
|
-
import { jsx as
|
|
2329
|
+
import { Box as Box4, Text as Text5 } from "ink";
|
|
2330
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1831
2331
|
var STATUS_DOT = {
|
|
1832
2332
|
online: { glyph: "\u25CF", color: "green" },
|
|
1833
2333
|
away: { glyph: "\u25D0", color: "yellow" },
|
|
@@ -1844,32 +2344,32 @@ function Header({
|
|
|
1844
2344
|
members,
|
|
1845
2345
|
presence
|
|
1846
2346
|
}) {
|
|
1847
|
-
return /* @__PURE__ */
|
|
1848
|
-
/* @__PURE__ */
|
|
1849
|
-
/* @__PURE__ */
|
|
1850
|
-
/* @__PURE__ */
|
|
1851
|
-
/* @__PURE__ */
|
|
1852
|
-
/* @__PURE__ */
|
|
2347
|
+
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
|
|
2348
|
+
/* @__PURE__ */ jsxs5(Box4, { justifyContent: "space-between", children: [
|
|
2349
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
2350
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "arp-tui" }),
|
|
2351
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u25B8 " }),
|
|
2352
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
1853
2353
|
channelSigil(channelKind),
|
|
1854
2354
|
sanitizeLabel(channelName)
|
|
1855
2355
|
] })
|
|
1856
2356
|
] }),
|
|
1857
|
-
/* @__PURE__ */
|
|
2357
|
+
/* @__PURE__ */ jsx5(Text5, { children: members.map((m) => {
|
|
1858
2358
|
const dot = STATUS_DOT[effectiveStatus(m, presence)];
|
|
1859
|
-
return /* @__PURE__ */
|
|
1860
|
-
/* @__PURE__ */
|
|
2359
|
+
return /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
2360
|
+
/* @__PURE__ */ jsxs5(Text5, { color: dot.color, children: [
|
|
1861
2361
|
dot.glyph,
|
|
1862
2362
|
" "
|
|
1863
2363
|
] }),
|
|
1864
|
-
/* @__PURE__ */
|
|
2364
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: effectiveStatus(m, presence) === "offline", children: [
|
|
1865
2365
|
m.type === "bot" ? "\u2699" : "",
|
|
1866
2366
|
sanitizeLabel(m.name, 20)
|
|
1867
2367
|
] }),
|
|
1868
|
-
/* @__PURE__ */
|
|
2368
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " })
|
|
1869
2369
|
] }, m.id);
|
|
1870
2370
|
}) })
|
|
1871
2371
|
] }),
|
|
1872
|
-
/* @__PURE__ */
|
|
2372
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns ?? 80, 100)) })
|
|
1873
2373
|
] });
|
|
1874
2374
|
}
|
|
1875
2375
|
|
|
@@ -1877,8 +2377,8 @@ function Header({
|
|
|
1877
2377
|
import { Static } from "ink";
|
|
1878
2378
|
|
|
1879
2379
|
// src/ui/MessageRow.tsx
|
|
1880
|
-
import { Box as
|
|
1881
|
-
import { jsx as
|
|
2380
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
2381
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1882
2382
|
function hhmm(rfc3339) {
|
|
1883
2383
|
const d = new Date(rfc3339);
|
|
1884
2384
|
return isNaN(d.getTime()) ? "--:--" : `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
@@ -1891,7 +2391,7 @@ function TokenChip({ message }) {
|
|
|
1891
2391
|
if (message.messageType !== "agent" || !message.tokensUsed) return null;
|
|
1892
2392
|
const parts = [`${formatTokens(message.tokensUsed)} tok`];
|
|
1893
2393
|
if (message.model) parts.push(sanitizeLabel(message.model, 16));
|
|
1894
|
-
return /* @__PURE__ */
|
|
2394
|
+
return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
1895
2395
|
message.verified ? "\u2713 " : "",
|
|
1896
2396
|
parts.join(" \xB7 ")
|
|
1897
2397
|
] });
|
|
@@ -1901,46 +2401,102 @@ function MessageRow({ message }) {
|
|
|
1901
2401
|
const isSystem = message.messageType === "system";
|
|
1902
2402
|
const name = sanitizeLabel(message.agentName, 20);
|
|
1903
2403
|
if (isSystem) {
|
|
1904
|
-
return /* @__PURE__ */
|
|
2404
|
+
return /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, italic: true, children: [
|
|
1905
2405
|
" ",
|
|
1906
2406
|
"\u2500\u2500 ",
|
|
1907
2407
|
sanitizeForTty(message.content),
|
|
1908
2408
|
" \u2500\u2500"
|
|
1909
2409
|
] }) });
|
|
1910
2410
|
}
|
|
1911
|
-
return /* @__PURE__ */
|
|
1912
|
-
/* @__PURE__ */
|
|
1913
|
-
/* @__PURE__ */
|
|
2411
|
+
return /* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", gap: 2, children: [
|
|
2412
|
+
/* @__PURE__ */ jsx6(Box5, { flexShrink: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
2413
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
1914
2414
|
hhmm(message.createdAt),
|
|
1915
2415
|
" "
|
|
1916
2416
|
] }),
|
|
1917
|
-
/* @__PURE__ */
|
|
2417
|
+
/* @__PURE__ */ jsxs6(Text6, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
|
|
1918
2418
|
isBot ? "\u2699" : "",
|
|
1919
2419
|
name.padEnd(12)
|
|
1920
2420
|
] }),
|
|
1921
|
-
/* @__PURE__ */
|
|
2421
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
1922
2422
|
" ",
|
|
1923
2423
|
sanitizeForTty(message.content)
|
|
1924
2424
|
] })
|
|
1925
2425
|
] }) }),
|
|
1926
|
-
/* @__PURE__ */
|
|
2426
|
+
/* @__PURE__ */ jsx6(Box5, { flexShrink: 0, children: /* @__PURE__ */ jsx6(TokenChip, { message }) })
|
|
1927
2427
|
] });
|
|
1928
2428
|
}
|
|
1929
2429
|
|
|
1930
2430
|
// src/ui/MessageList.tsx
|
|
1931
|
-
import { jsx as
|
|
2431
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1932
2432
|
function MessageList({ messages }) {
|
|
1933
|
-
return /* @__PURE__ */
|
|
2433
|
+
return /* @__PURE__ */ jsx7(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx7(MessageRow, { message: m }, m.id) });
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// src/ui/NotificationsPanel.tsx
|
|
2437
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
2438
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2439
|
+
function relativeAge(createdAt) {
|
|
2440
|
+
const ms = Date.now() - new Date(createdAt).getTime();
|
|
2441
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
2442
|
+
if (ms < 1e4) return "just now";
|
|
2443
|
+
if (ms < 9e4) return `${Math.round(ms / 1e3)}s`;
|
|
2444
|
+
if (ms < 90 * 6e4) return `${Math.round(ms / 6e4)}m`;
|
|
2445
|
+
if (ms < 48 * 36e5) return `${Math.round(ms / 36e5)}h`;
|
|
2446
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
2447
|
+
}
|
|
2448
|
+
function shortRef(entityRef) {
|
|
2449
|
+
return sanitizeLabel(entityRef, 24);
|
|
2450
|
+
}
|
|
2451
|
+
function NotificationsPanel({
|
|
2452
|
+
notifications,
|
|
2453
|
+
notifSelected
|
|
2454
|
+
}) {
|
|
2455
|
+
const unreadCount = notifications.filter((n) => !n.readAt).length;
|
|
2456
|
+
return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2457
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
|
|
2458
|
+
"alerts",
|
|
2459
|
+
" ",
|
|
2460
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2461
|
+
"(",
|
|
2462
|
+
unreadCount,
|
|
2463
|
+
" unread \xB7 ",
|
|
2464
|
+
notifications.length,
|
|
2465
|
+
" total)"
|
|
2466
|
+
] })
|
|
2467
|
+
] }),
|
|
2468
|
+
notifications.length === 0 ? /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "no notifications" }) : notifications.map((n, i) => {
|
|
2469
|
+
const isSelected = i === notifSelected;
|
|
2470
|
+
const isUnread = n.readAt === null;
|
|
2471
|
+
const type = sanitizeLabel(n.type, 20);
|
|
2472
|
+
const ref = shortRef(sanitizeForTty(n.entityRef));
|
|
2473
|
+
const age = relativeAge(n.createdAt);
|
|
2474
|
+
return /* @__PURE__ */ jsxs7(Text7, { inverse: isSelected, children: [
|
|
2475
|
+
isUnread ? /* @__PURE__ */ jsx8(Text7, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " " }),
|
|
2476
|
+
/* @__PURE__ */ jsx8(Text7, { bold: isSelected, children: type }),
|
|
2477
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2478
|
+
" ",
|
|
2479
|
+
ref
|
|
2480
|
+
] }),
|
|
2481
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2482
|
+
" ",
|
|
2483
|
+
age
|
|
2484
|
+
] })
|
|
2485
|
+
] }, n.id);
|
|
2486
|
+
}),
|
|
2487
|
+
/* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "J/K move \xB7 d dismiss \xB7 a mark all read \xB7 Esc close" })
|
|
2488
|
+
] });
|
|
1934
2489
|
}
|
|
1935
2490
|
|
|
1936
2491
|
// src/ui/StatusBar.tsx
|
|
1937
|
-
import { Text as
|
|
1938
|
-
import { Fragment, jsx as
|
|
2492
|
+
import { Text as Text8 } from "ink";
|
|
2493
|
+
import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1939
2494
|
var MODE_LABEL = {
|
|
1940
2495
|
normal: "NORMAL",
|
|
1941
2496
|
insert: "INSERT",
|
|
1942
2497
|
switcher: "CHANNELS",
|
|
1943
|
-
token: "TOKEN"
|
|
2498
|
+
token: "TOKEN",
|
|
2499
|
+
notifications: "ALERTS"
|
|
1944
2500
|
};
|
|
1945
2501
|
function ageLabel(obtainedAt) {
|
|
1946
2502
|
if (!obtainedAt) return "\u2014";
|
|
@@ -1971,6 +2527,7 @@ function StatusBar({
|
|
|
1971
2527
|
tokenObtainedAt,
|
|
1972
2528
|
tokenRefreshable,
|
|
1973
2529
|
clerkRefresh,
|
|
2530
|
+
notifCount,
|
|
1974
2531
|
error
|
|
1975
2532
|
}) {
|
|
1976
2533
|
const connColor = connection === "connected" ? "green" : connection === "reconnecting" ? "yellow" : "red";
|
|
@@ -1990,37 +2547,41 @@ function StatusBar({
|
|
|
1990
2547
|
color: stale ? "yellow" : "green"
|
|
1991
2548
|
};
|
|
1992
2549
|
}
|
|
1993
|
-
return /* @__PURE__ */
|
|
1994
|
-
/* @__PURE__ */
|
|
2550
|
+
return /* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2551
|
+
/* @__PURE__ */ jsxs8(Text8, { bold: true, color: mode === "insert" ? "cyan" : mode === "token" ? "yellow" : "white", children: [
|
|
1995
2552
|
" ",
|
|
1996
2553
|
MODE_LABEL[mode],
|
|
1997
2554
|
" "
|
|
1998
2555
|
] }),
|
|
1999
|
-
/* @__PURE__ */
|
|
2000
|
-
/* @__PURE__ */
|
|
2001
|
-
/* @__PURE__ */
|
|
2002
|
-
/* @__PURE__ */
|
|
2003
|
-
/* @__PURE__ */
|
|
2556
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "\u2502 " }),
|
|
2557
|
+
/* @__PURE__ */ jsx9(Text8, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
|
|
2558
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 " }),
|
|
2559
|
+
/* @__PURE__ */ jsx9(Text8, { color: tokenSeg.color, children: tokenSeg.text }),
|
|
2560
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2004
2561
|
" \u2502 seq ",
|
|
2005
2562
|
lastSeq ?? "\u2014"
|
|
2006
2563
|
] }),
|
|
2564
|
+
notifCount > 0 ? /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
|
|
2565
|
+
" \u2502 alerts:",
|
|
2566
|
+
notifCount
|
|
2567
|
+
] }) : null,
|
|
2007
2568
|
error ? (
|
|
2008
2569
|
// BL-222: an error replaces the full hint line but must never strand
|
|
2009
2570
|
// the user — keep the exit hint visible alongside it
|
|
2010
|
-
/* @__PURE__ */
|
|
2011
|
-
/* @__PURE__ */
|
|
2571
|
+
/* @__PURE__ */ jsxs8(Fragment, { children: [
|
|
2572
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
|
|
2012
2573
|
" \u2502 ",
|
|
2013
2574
|
sanitizeForTty(error)
|
|
2014
2575
|
] }),
|
|
2015
|
-
/* @__PURE__ */
|
|
2576
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 q quit" })
|
|
2016
2577
|
] })
|
|
2017
|
-
) : /* @__PURE__ */
|
|
2578
|
+
) : /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 c channels \xB7 a alerts \xB7 n next \xB7 v watch \xB7 i compose \xB7 r reconnect \xB7 Esc normal \xB7 q quit" })
|
|
2018
2579
|
] });
|
|
2019
2580
|
}
|
|
2020
2581
|
|
|
2021
2582
|
// src/ui/TokenPrompt.tsx
|
|
2022
|
-
import { Box as
|
|
2023
|
-
import { jsx as
|
|
2583
|
+
import { Box as Box7, Text as Text9 } from "ink";
|
|
2584
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2024
2585
|
function TokenPrompt({
|
|
2025
2586
|
value,
|
|
2026
2587
|
reason,
|
|
@@ -2028,26 +2589,27 @@ function TokenPrompt({
|
|
|
2028
2589
|
jwtTemplate
|
|
2029
2590
|
}) {
|
|
2030
2591
|
const masked = value.length === 0 ? "" : value.length <= 16 ? value : `${value.slice(0, 12)}\u2026 (${value.length} chars)`;
|
|
2031
|
-
return /* @__PURE__ */
|
|
2032
|
-
/* @__PURE__ */
|
|
2033
|
-
reason ? /* @__PURE__ */
|
|
2034
|
-
/* @__PURE__ */
|
|
2592
|
+
return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2593
|
+
/* @__PURE__ */ jsx10(Text9, { bold: true, color: "yellow", children: "token needed" }),
|
|
2594
|
+
reason ? /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: sanitizeForTty(reason) }) : null,
|
|
2595
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2035
2596
|
"mint one: open ",
|
|
2036
2597
|
webUrl,
|
|
2037
2598
|
" logged in, browser console:"
|
|
2038
2599
|
] }),
|
|
2039
|
-
/* @__PURE__ */
|
|
2040
|
-
/* @__PURE__ */
|
|
2041
|
-
/* @__PURE__ */
|
|
2042
|
-
/* @__PURE__ */
|
|
2043
|
-
/* @__PURE__ */
|
|
2600
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
|
|
2601
|
+
/* @__PURE__ */ jsxs9(Text9, { children: [
|
|
2602
|
+
/* @__PURE__ */ jsx10(Text9, { color: "yellow", children: "> " }),
|
|
2603
|
+
/* @__PURE__ */ jsx10(Text9, { children: masked }),
|
|
2604
|
+
/* @__PURE__ */ jsx10(Text9, { inverse: true, children: " " })
|
|
2044
2605
|
] }),
|
|
2045
|
-
/* @__PURE__ */
|
|
2606
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "paste, then Enter \xB7 Ctrl+C quits" })
|
|
2046
2607
|
] });
|
|
2047
2608
|
}
|
|
2048
2609
|
|
|
2049
2610
|
// src/app.tsx
|
|
2050
|
-
import { jsx as
|
|
2611
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2612
|
+
var ACTIVITY_SETTLE_MS2 = 4e3;
|
|
2051
2613
|
function cleanInput(input) {
|
|
2052
2614
|
return input.replace(/\r\n|[\r\n]/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
|
|
2053
2615
|
}
|
|
@@ -2083,17 +2645,28 @@ function App({ store: store2 }) {
|
|
|
2083
2645
|
else if (input) store2.setDraft(s.draft + cleanInput(input));
|
|
2084
2646
|
return;
|
|
2085
2647
|
}
|
|
2648
|
+
if (s.mode === "notifications") {
|
|
2649
|
+
if (key.escape) store2.setMode("normal");
|
|
2650
|
+
else if (input === "j" || key.downArrow) store2.moveNotifSelection(1);
|
|
2651
|
+
else if (input === "k" || key.upArrow) store2.moveNotifSelection(-1);
|
|
2652
|
+
else if (input === "d" || key.return) {
|
|
2653
|
+
const n = s.notifications[s.notifSelected];
|
|
2654
|
+
if (n) store2.dismissNotification(n.id);
|
|
2655
|
+
} else if (input === "a") store2.markAllNotificationsRead();
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2086
2658
|
if (s.mode === "switcher") {
|
|
2087
2659
|
const filtered = filterChannels(s.channels, s.switcherFilter);
|
|
2660
|
+
const sorted = sortChannelsByPriority(filtered, { attention: s.attention, unread: s.unread });
|
|
2088
2661
|
const filterEmpty = s.switcherFilter.length === 0;
|
|
2089
2662
|
if (key.escape) store2.setMode("normal");
|
|
2090
2663
|
else if (key.return) {
|
|
2091
2664
|
if (filterEmpty) store2.setMode("normal");
|
|
2092
|
-
else if (
|
|
2665
|
+
else if (sorted[0]) store2.openChannel(sorted[0].id);
|
|
2093
2666
|
} else if (key.backspace || key.delete) {
|
|
2094
2667
|
store2.setSwitcherFilter(s.switcherFilter.slice(0, -1));
|
|
2095
2668
|
} else if (filterEmpty && /^[1-9]$/.test(input)) {
|
|
2096
|
-
const ch =
|
|
2669
|
+
const ch = sorted[Number(input) - 1];
|
|
2097
2670
|
if (ch) store2.openChannel(ch.id);
|
|
2098
2671
|
} else if (input) {
|
|
2099
2672
|
store2.setSwitcherFilter(s.switcherFilter + cleanInput(input));
|
|
@@ -2104,14 +2677,26 @@ function App({ store: store2 }) {
|
|
|
2104
2677
|
store2.quit();
|
|
2105
2678
|
exit();
|
|
2106
2679
|
} else if (input === "c") store2.setMode("switcher");
|
|
2680
|
+
else if (input === "a") store2.setMode("notifications");
|
|
2107
2681
|
else if (input === "i" || key.return) store2.setMode("insert");
|
|
2108
2682
|
else if (input === "r") store2.reconnectNow();
|
|
2683
|
+
else if (input === "n") store2.jumpNextActive();
|
|
2684
|
+
else if (input === "v") store2.toggleActivityPin();
|
|
2109
2685
|
},
|
|
2110
2686
|
{ isActive: interactive }
|
|
2111
2687
|
);
|
|
2112
2688
|
const activeChannel = state.channels.find((c) => c.id === state.activeChannelId);
|
|
2113
|
-
|
|
2114
|
-
|
|
2689
|
+
const lastKey = (r) => {
|
|
2690
|
+
const ks = Object.keys(r);
|
|
2691
|
+
return ks[ks.length - 1];
|
|
2692
|
+
};
|
|
2693
|
+
const activityRows = state.activityByTurn[lastKey(state.activityByTurn) ?? ""] ?? [];
|
|
2694
|
+
const activityPlan = state.plansByTurn[lastKey(state.plansByTurn) ?? ""] ?? [];
|
|
2695
|
+
const activityLive = activityRows.some((r) => r.status === "pending" || r.status === "in_progress") || activityPlan.some((e) => e.status !== "completed");
|
|
2696
|
+
const activityWithinSettle = state.activityIdleSince !== null && Date.now() - state.activityIdleSince < ACTIVITY_SETTLE_MS2;
|
|
2697
|
+
const activityVisible = state.activityPinned || activityLive || activityWithinSettle;
|
|
2698
|
+
return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
|
|
2699
|
+
/* @__PURE__ */ jsx11(
|
|
2115
2700
|
Header,
|
|
2116
2701
|
{
|
|
2117
2702
|
channelName: activeChannel?.name ?? "(connecting\u2026)",
|
|
@@ -2120,16 +2705,26 @@ function App({ store: store2 }) {
|
|
|
2120
2705
|
presence: state.presence
|
|
2121
2706
|
}
|
|
2122
2707
|
),
|
|
2123
|
-
/* @__PURE__ */
|
|
2124
|
-
|
|
2708
|
+
/* @__PURE__ */ jsx11(MessageList, { messages: state.messages }),
|
|
2709
|
+
/* @__PURE__ */ jsx11(ActivitySurface, { toolCalls: activityRows, plan: activityPlan, visible: activityVisible, pinned: state.activityPinned }),
|
|
2710
|
+
state.mode === "switcher" ? /* @__PURE__ */ jsx11(
|
|
2125
2711
|
ChannelSwitcher,
|
|
2126
2712
|
{
|
|
2127
2713
|
channels: state.channels,
|
|
2128
2714
|
activeChannelId: state.activeChannelId,
|
|
2129
|
-
filter: state.switcherFilter
|
|
2715
|
+
filter: state.switcherFilter,
|
|
2716
|
+
attention: state.attention,
|
|
2717
|
+
unread: state.unread
|
|
2718
|
+
}
|
|
2719
|
+
) : null,
|
|
2720
|
+
state.mode === "notifications" ? /* @__PURE__ */ jsx11(
|
|
2721
|
+
NotificationsPanel,
|
|
2722
|
+
{
|
|
2723
|
+
notifications: state.notifications,
|
|
2724
|
+
notifSelected: state.notifSelected
|
|
2130
2725
|
}
|
|
2131
2726
|
) : null,
|
|
2132
|
-
state.mode === "token" ? /* @__PURE__ */
|
|
2727
|
+
state.mode === "token" ? /* @__PURE__ */ jsx11(
|
|
2133
2728
|
TokenPrompt,
|
|
2134
2729
|
{
|
|
2135
2730
|
value: state.tokenPrompt,
|
|
@@ -2138,9 +2733,9 @@ function App({ store: store2 }) {
|
|
|
2138
2733
|
jwtTemplate: store2.instance.clerkJwtTemplate
|
|
2139
2734
|
}
|
|
2140
2735
|
) : null,
|
|
2141
|
-
/* @__PURE__ */
|
|
2142
|
-
/* @__PURE__ */
|
|
2143
|
-
/* @__PURE__ */
|
|
2736
|
+
/* @__PURE__ */ jsx11(ActivityLine, { activities: state.activities }),
|
|
2737
|
+
/* @__PURE__ */ jsx11(Composer, { draft: state.draft, focused: state.mode === "insert" }),
|
|
2738
|
+
/* @__PURE__ */ jsx11(
|
|
2144
2739
|
StatusBar,
|
|
2145
2740
|
{
|
|
2146
2741
|
mode: state.mode,
|
|
@@ -2152,15 +2747,95 @@ function App({ store: store2 }) {
|
|
|
2152
2747
|
tokenObtainedAt: state.tokenObtainedAt,
|
|
2153
2748
|
tokenRefreshable: state.tokenRefreshable,
|
|
2154
2749
|
clerkRefresh: state.clerkRefresh,
|
|
2750
|
+
notifCount: state.notifications.filter((n) => !n.readAt).length,
|
|
2155
2751
|
error: state.error
|
|
2156
2752
|
}
|
|
2157
2753
|
),
|
|
2158
|
-
!interactive ? /* @__PURE__ */
|
|
2754
|
+
!interactive ? /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(non-interactive terminal: keys disabled \u2014 tail is read-only here)" }) : null
|
|
2159
2755
|
] });
|
|
2160
2756
|
}
|
|
2161
2757
|
|
|
2758
|
+
// src/commands/profile.ts
|
|
2759
|
+
var USAGE = `usage: arp-tui profile <list|add|switch|remove> [...]
|
|
2760
|
+
|
|
2761
|
+
profile list [--json]
|
|
2762
|
+
show every saved instance profile (the active one is marked *)
|
|
2763
|
+
|
|
2764
|
+
profile add <name> --relay <url> [--web <url>] [--clerk-fapi <url>]
|
|
2765
|
+
[--clerk-template <name>] [--clerk-oauth-client-id <id>]
|
|
2766
|
+
define a new instance. --web and --clerk-fapi are required unless --relay
|
|
2767
|
+
is the built-in dev relay. Does not change the active profile.
|
|
2768
|
+
|
|
2769
|
+
profile switch <name>
|
|
2770
|
+
make <name> the active profile
|
|
2771
|
+
|
|
2772
|
+
profile remove <name>
|
|
2773
|
+
delete a profile (not the active one, not the last one)`;
|
|
2774
|
+
function requireName(name, verb) {
|
|
2775
|
+
if (!name) throw new Error(`profile ${verb} requires a profile name \u2014 see \`arp-tui profile\`.`);
|
|
2776
|
+
return name;
|
|
2777
|
+
}
|
|
2778
|
+
function runProfileCommand(args2) {
|
|
2779
|
+
try {
|
|
2780
|
+
switch (args2.sub) {
|
|
2781
|
+
case "list": {
|
|
2782
|
+
const { active, profiles } = listProfiles();
|
|
2783
|
+
if (args2.json) {
|
|
2784
|
+
console.log(JSON.stringify({ active, profiles }, null, 2));
|
|
2785
|
+
} else {
|
|
2786
|
+
const names = Object.keys(profiles);
|
|
2787
|
+
if (names.length === 0) {
|
|
2788
|
+
console.log("(no profiles)");
|
|
2789
|
+
} else {
|
|
2790
|
+
for (const name of names) {
|
|
2791
|
+
const mark = name === active ? "*" : " ";
|
|
2792
|
+
console.log(`${mark} ${name.padEnd(16)} ${profiles[name].relayUrl}`);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
process.exit(0);
|
|
2797
|
+
}
|
|
2798
|
+
case "add": {
|
|
2799
|
+
const name = requireName(args2.name, "add");
|
|
2800
|
+
if (!args2.relay) throw new Error("profile add requires --relay <url>.");
|
|
2801
|
+
const raw = { relayUrl: args2.relay };
|
|
2802
|
+
if (args2.web) raw["webUrl"] = args2.web;
|
|
2803
|
+
if (args2.clerkFapi) raw["clerkFapiUrl"] = args2.clerkFapi;
|
|
2804
|
+
if (args2.clerkTemplate) raw["clerkJwtTemplate"] = args2.clerkTemplate;
|
|
2805
|
+
if (args2.clerkOAuthClientId) raw["clerkOAuthClientId"] = args2.clerkOAuthClientId;
|
|
2806
|
+
const profile = addProfile(name, raw);
|
|
2807
|
+
console.error(`\u2713 added profile "${name}" (${profile.relayUrl})`);
|
|
2808
|
+
console.error(` run \`arp-tui profile switch ${name}\` to make it active`);
|
|
2809
|
+
process.exit(0);
|
|
2810
|
+
}
|
|
2811
|
+
case "switch": {
|
|
2812
|
+
const name = requireName(args2.name, "switch");
|
|
2813
|
+
const profile = switchProfile(name);
|
|
2814
|
+
console.error(`\u2713 active profile is now "${name}" (${profile.relayUrl})`);
|
|
2815
|
+
process.exit(0);
|
|
2816
|
+
}
|
|
2817
|
+
case "remove": {
|
|
2818
|
+
const name = requireName(args2.name, "remove");
|
|
2819
|
+
removeProfile(name);
|
|
2820
|
+
console.error(`\u2713 removed profile "${name}"`);
|
|
2821
|
+
console.error(" (stored credentials for its relay remain \u2014 `arp-tui logout` clears those)");
|
|
2822
|
+
process.exit(0);
|
|
2823
|
+
}
|
|
2824
|
+
default: {
|
|
2825
|
+
console.error(USAGE);
|
|
2826
|
+
if (args2.sub) console.error(`
|
|
2827
|
+
unknown profile command "${args2.sub}".`);
|
|
2828
|
+
process.exit(1);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2833
|
+
process.exit(1);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2162
2837
|
// src/index.tsx
|
|
2163
|
-
import { jsx as
|
|
2838
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
2164
2839
|
function helpText(profileName2, base2, effective) {
|
|
2165
2840
|
const relayOverridden = effective.relayUrl !== base2.relayUrl;
|
|
2166
2841
|
const overrideNote = relayOverridden ? `
|
|
@@ -2169,7 +2844,7 @@ function helpText(profileName2, base2, effective) {
|
|
|
2169
2844
|
instance \u2014 a JWT minted there will NOT work against the override)` : "";
|
|
2170
2845
|
return `arp-tui \u2014 tail + post for Agent Relay Protocol channels (as the HUMAN)
|
|
2171
2846
|
|
|
2172
|
-
usage: arp-tui [tail|login|logout|status] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
2847
|
+
usage: arp-tui [tail|login|logout|status|profile] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
2173
2848
|
|
|
2174
2849
|
commands:
|
|
2175
2850
|
tail live channel tail + post (default)
|
|
@@ -2177,6 +2852,7 @@ commands:
|
|
|
2177
2852
|
stores refreshable tokens in ~/.arp-tui/credentials.json
|
|
2178
2853
|
logout revoke the OAuth grant and delete this instance's stored tokens
|
|
2179
2854
|
status show the stored credential state for the active instance
|
|
2855
|
+
profile manage saved instances (list|add|switch|remove; run \`arp-tui profile\`)
|
|
2180
2856
|
|
|
2181
2857
|
options:
|
|
2182
2858
|
--profile <name> instance profile from ~/.arp-tui/config.json
|
|
@@ -2206,6 +2882,11 @@ function parseCliArgs(argv) {
|
|
|
2206
2882
|
profile: { type: "string" },
|
|
2207
2883
|
relay: { type: "string" },
|
|
2208
2884
|
token: { type: "string" },
|
|
2885
|
+
web: { type: "string" },
|
|
2886
|
+
"clerk-fapi": { type: "string" },
|
|
2887
|
+
"clerk-template": { type: "string" },
|
|
2888
|
+
"clerk-oauth-client-id": { type: "string" },
|
|
2889
|
+
json: { type: "boolean" },
|
|
2209
2890
|
help: { type: "boolean", short: "h" }
|
|
2210
2891
|
},
|
|
2211
2892
|
allowPositionals: true
|
|
@@ -2216,9 +2897,16 @@ function parseCliArgs(argv) {
|
|
|
2216
2897
|
}
|
|
2217
2898
|
return {
|
|
2218
2899
|
cmd: parsed.positionals[0] ?? "tail",
|
|
2900
|
+
sub: parsed.positionals[1],
|
|
2901
|
+
name: parsed.positionals[2],
|
|
2219
2902
|
profile: parsed.values.profile,
|
|
2220
2903
|
relay: parsed.values.relay,
|
|
2221
2904
|
token: parsed.values.token,
|
|
2905
|
+
web: parsed.values.web,
|
|
2906
|
+
clerkFapi: parsed.values["clerk-fapi"],
|
|
2907
|
+
clerkTemplate: parsed.values["clerk-template"],
|
|
2908
|
+
clerkOAuthClientId: parsed.values["clerk-oauth-client-id"],
|
|
2909
|
+
json: parsed.values.json === true,
|
|
2222
2910
|
help: parsed.values.help === true
|
|
2223
2911
|
};
|
|
2224
2912
|
}
|
|
@@ -2246,6 +2934,18 @@ try {
|
|
|
2246
2934
|
console.error(err instanceof Error ? err.message : String(err));
|
|
2247
2935
|
process.exit(1);
|
|
2248
2936
|
}
|
|
2937
|
+
if (args.cmd === "profile") {
|
|
2938
|
+
runProfileCommand({
|
|
2939
|
+
sub: args.sub,
|
|
2940
|
+
name: args.name,
|
|
2941
|
+
relay: args.relay,
|
|
2942
|
+
web: args.web,
|
|
2943
|
+
clerkFapi: args.clerkFapi,
|
|
2944
|
+
clerkTemplate: args.clerkTemplate,
|
|
2945
|
+
clerkOAuthClientId: args.clerkOAuthClientId,
|
|
2946
|
+
json: args.json
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2249
2949
|
var profileName;
|
|
2250
2950
|
var base;
|
|
2251
2951
|
var instance;
|
|
@@ -2330,4 +3030,4 @@ if (credentials?.tokenType === "dev-jwt" && credentials.token && !credentials.cl
|
|
|
2330
3030
|
);
|
|
2331
3031
|
}
|
|
2332
3032
|
var store = createStore({ instance, credentials });
|
|
2333
|
-
render(/* @__PURE__ */
|
|
3033
|
+
render(/* @__PURE__ */ jsx12(App, { store }));
|