@interactive-inc/claude-funnel 0.29.0 → 0.31.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 +2 -12
- package/dist/bin.js +489 -924
- package/dist/connectors/discord.d.ts +1 -1
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/slack.d.ts +1 -1
- package/dist/connectors/slack.js +1 -1
- package/dist/{discord-connector-schema-RzDvrNE5.js → discord-connector-schema-BeThExJp.js} +5 -3
- package/dist/{discord-connector-schema-Df_McRJI.d.ts → discord-connector-schema-DF4pL3Sc.d.ts} +4 -2
- package/dist/gateway/daemon.js +223 -201
- package/dist/index.d.ts +9 -9
- package/dist/index.js +28 -2225
- package/dist/{resolve-connector-token-Ch6XWMJM.js → resolve-connector-token-BHmZLRrV.js} +1 -1
- package/dist/{slack-connector-schema-CHbRJHGp.js → slack-connector-schema-DDbSGPZn.js} +6 -7
- package/dist/{slack-listener-CLTiOEJw.d.ts → slack-listener-9UdAn_ui.d.ts} +5 -6
- package/package.json +1 -5
- package/dist/highlights-eq9cgrbb.scm +0 -604
- package/dist/highlights-ghv9g403.scm +0 -205
- package/dist/highlights-hk7bwhj4.scm +0 -284
- package/dist/highlights-r812a2qc.scm +0 -150
- package/dist/highlights-x6tmsnaa.scm +0 -115
- package/dist/injections-73j83es3.scm +0 -27
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-
|
|
1
|
+
import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-BeThExJp.js";
|
|
2
2
|
import { n as FunnelConnectorListener, t as FunnelLogger } from "./logger-Czli2OKh.js";
|
|
3
3
|
import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-eYE4g77K.js";
|
|
4
4
|
import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConnectorSchema, o as NodeFunnelFileSystem, r as scheduleEntrySchema, s as FunnelFileSystem, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CM-sRkac.js";
|
|
5
|
-
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-
|
|
5
|
+
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-DDbSGPZn.js";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { z } from "zod";
|
|
@@ -18,10 +18,6 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
19
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
20
20
|
import { stringify } from "yaml";
|
|
21
|
-
import { TextAttributes, createCliRenderer } from "@opentui/core";
|
|
22
|
-
import { createRoot, useKeyboard, useRenderer } from "@opentui/react";
|
|
23
|
-
import { createContext, useContext, useEffect, useId, useState } from "react";
|
|
24
|
-
import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
|
|
25
21
|
//#region lib/engine/id/id-generator.ts
|
|
26
22
|
/**
|
|
27
23
|
* ID generator boundary. Default NodeFunnelIdGenerator wraps `crypto.randomUUID()`;
|
|
@@ -187,7 +183,7 @@ var FunnelSettingsStore = class extends FunnelSettingsReader {
|
|
|
187
183
|
...settings,
|
|
188
184
|
version: 1
|
|
189
185
|
};
|
|
190
|
-
this.fs.
|
|
186
|
+
this.fs.writeSecretFileSync(this.path, `${JSON.stringify(versioned, null, 2)}\n`);
|
|
191
187
|
}
|
|
192
188
|
};
|
|
193
189
|
//#endregion
|
|
@@ -1807,7 +1803,7 @@ const resolveDaemonScript = () => {
|
|
|
1807
1803
|
const DEFAULT_PORT$1 = 9742;
|
|
1808
1804
|
const STARTUP_TIMEOUT_MS = 5e3;
|
|
1809
1805
|
const SIGTERM_TIMEOUT_MS = 2e3;
|
|
1810
|
-
const POLL_INTERVAL_MS
|
|
1806
|
+
const POLL_INTERVAL_MS = 100;
|
|
1811
1807
|
const SIGKILL_GRACE_MS = 200;
|
|
1812
1808
|
const defaultProcess$1 = new NodeFunnelProcessRunner();
|
|
1813
1809
|
const defaultFs$1 = new NodeFunnelFileSystem();
|
|
@@ -1869,7 +1865,7 @@ var FunnelGateway = class {
|
|
|
1869
1865
|
const deadline = this.clock.millis() + STARTUP_TIMEOUT_MS;
|
|
1870
1866
|
while (this.clock.millis() < deadline) {
|
|
1871
1867
|
if (this.isRunning()) return true;
|
|
1872
|
-
await this.sleep(POLL_INTERVAL_MS
|
|
1868
|
+
await this.sleep(POLL_INTERVAL_MS);
|
|
1873
1869
|
}
|
|
1874
1870
|
return this.isRunning();
|
|
1875
1871
|
}
|
|
@@ -1906,7 +1902,7 @@ var FunnelGateway = class {
|
|
|
1906
1902
|
this.removePid();
|
|
1907
1903
|
return true;
|
|
1908
1904
|
}
|
|
1909
|
-
await this.sleep(POLL_INTERVAL_MS
|
|
1905
|
+
await this.sleep(POLL_INTERVAL_MS);
|
|
1910
1906
|
}
|
|
1911
1907
|
try {
|
|
1912
1908
|
this.process.kill(pid, "SIGKILL");
|
|
@@ -2558,7 +2554,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
|
|
|
2558
2554
|
record(record) {
|
|
2559
2555
|
const event = {
|
|
2560
2556
|
type: record.meta?.event_type ?? "unknown",
|
|
2561
|
-
content: truncate
|
|
2557
|
+
content: truncate(record.content),
|
|
2562
2558
|
channel_id: record.channelId,
|
|
2563
2559
|
connector_id: record.connectorId,
|
|
2564
2560
|
meta: record.meta
|
|
@@ -2615,7 +2611,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
|
|
|
2615
2611
|
this.sink.close();
|
|
2616
2612
|
}
|
|
2617
2613
|
};
|
|
2618
|
-
function truncate
|
|
2614
|
+
function truncate(content) {
|
|
2619
2615
|
if (content.length <= MAX_CONTENT_CHARS) return content;
|
|
2620
2616
|
return `${content.slice(0, MAX_CONTENT_CHARS)}...`;
|
|
2621
2617
|
}
|
|
@@ -3047,6 +3043,13 @@ const gatewayRoutes = factory$1.createApp().get("/health", ...healthHandler).get
|
|
|
3047
3043
|
//#endregion
|
|
3048
3044
|
//#region lib/gateway/gateway-server.ts
|
|
3049
3045
|
const DEFAULT_PORT = 9742;
|
|
3046
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
3047
|
+
const LOOPBACK_HOSTS = new Set([
|
|
3048
|
+
"127.0.0.1",
|
|
3049
|
+
"localhost",
|
|
3050
|
+
"::1",
|
|
3051
|
+
"::ffff:127.0.0.1"
|
|
3052
|
+
]);
|
|
3050
3053
|
const defaultDbPath = () => join(funnelTmpDir(), "events.db");
|
|
3051
3054
|
const defaultOnError = () => {};
|
|
3052
3055
|
/**
|
|
@@ -3063,6 +3066,7 @@ var FunnelGatewayServer = class {
|
|
|
3063
3066
|
channels;
|
|
3064
3067
|
settings;
|
|
3065
3068
|
port;
|
|
3069
|
+
hostname;
|
|
3066
3070
|
dbPath;
|
|
3067
3071
|
process;
|
|
3068
3072
|
logger;
|
|
@@ -3082,6 +3086,7 @@ var FunnelGatewayServer = class {
|
|
|
3082
3086
|
this.channels = deps.channels;
|
|
3083
3087
|
this.settings = deps.settings;
|
|
3084
3088
|
this.port = deps.port ?? DEFAULT_PORT;
|
|
3089
|
+
this.hostname = deps.hostname ?? DEFAULT_HOST;
|
|
3085
3090
|
this.dbPath = deps.dbPath ?? defaultDbPath();
|
|
3086
3091
|
this.process = deps.process;
|
|
3087
3092
|
this.logger = deps.logger;
|
|
@@ -3126,10 +3131,12 @@ var FunnelGatewayServer = class {
|
|
|
3126
3131
|
}
|
|
3127
3132
|
async start() {
|
|
3128
3133
|
if (this.server) return this.server;
|
|
3134
|
+
if (!this.token && !LOOPBACK_HOSTS.has(this.hostname)) this.logger?.warn("gateway auth is disabled on a non-loopback bind — every endpoint is reachable without a token", { hostname: this.hostname });
|
|
3129
3135
|
const app = this.buildApp();
|
|
3130
3136
|
this.startedAt = this.nowMs();
|
|
3131
3137
|
this.server = Bun.serve({
|
|
3132
3138
|
port: this.port,
|
|
3139
|
+
hostname: this.hostname,
|
|
3133
3140
|
development: false,
|
|
3134
3141
|
fetch: (request, server) => this.handleFetch(request, server, app),
|
|
3135
3142
|
websocket: {
|
|
@@ -3508,7 +3515,7 @@ const noopOnError = () => {};
|
|
|
3508
3515
|
/**
|
|
3509
3516
|
* Facade exposing every funnel facet as a getter.
|
|
3510
3517
|
*
|
|
3511
|
-
* The same `Funnel` is used by the CLI
|
|
3518
|
+
* The same `Funnel` is used by the CLI and as a programmable library.
|
|
3512
3519
|
* All side-effecting boundaries (filesystem, process, logger, clock, id, paths) are
|
|
3513
3520
|
* injectable via `Props` — passing memory implementations gives a fully sandboxed
|
|
3514
3521
|
* Funnel that touches no real disk, processes, or wall-clock time.
|
|
@@ -3734,6 +3741,7 @@ var Funnel = class Funnel {
|
|
|
3734
3741
|
channels: this.channels,
|
|
3735
3742
|
settings: this.store,
|
|
3736
3743
|
port: options.port,
|
|
3744
|
+
hostname: options.hostname,
|
|
3737
3745
|
dbPath: options.dbPath,
|
|
3738
3746
|
eventLog: options.eventLog,
|
|
3739
3747
|
process: this.process,
|
|
@@ -4437,18 +4445,18 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
4437
4445
|
});
|
|
4438
4446
|
}
|
|
4439
4447
|
queryRaw(query) {
|
|
4440
|
-
return takeRecent(this.raws.filter((event) => matches
|
|
4448
|
+
return takeRecent(this.raws.filter((event) => matches(event, query)), query.limit);
|
|
4441
4449
|
}
|
|
4442
4450
|
queryProcessed(query) {
|
|
4443
4451
|
return takeRecent(this.processeds.filter((event) => {
|
|
4444
|
-
if (!matches
|
|
4452
|
+
if (!matches(event, query)) return false;
|
|
4445
4453
|
if (query.outcome !== void 0 && event.outcome !== query.outcome) return false;
|
|
4446
4454
|
return true;
|
|
4447
4455
|
}), query.limit);
|
|
4448
4456
|
}
|
|
4449
4457
|
queryConnection(query) {
|
|
4450
4458
|
return takeRecent(this.connections.filter((event) => {
|
|
4451
|
-
if (!matches
|
|
4459
|
+
if (!matches(event, query)) return false;
|
|
4452
4460
|
if (query.status !== void 0 && event.status !== query.status) return false;
|
|
4453
4461
|
return true;
|
|
4454
4462
|
}), query.limit);
|
|
@@ -4460,7 +4468,7 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
4460
4468
|
}
|
|
4461
4469
|
close() {}
|
|
4462
4470
|
};
|
|
4463
|
-
const matches
|
|
4471
|
+
const matches = (event, query) => {
|
|
4464
4472
|
if (query.type !== void 0 && event.type !== query.type) return false;
|
|
4465
4473
|
if (query.connectorId !== void 0 && event.connectorId !== query.connectorId) return false;
|
|
4466
4474
|
if (query.channelId !== void 0 && event.channelId !== query.channelId) return false;
|
|
@@ -5197,7 +5205,7 @@ examples:
|
|
|
5197
5205
|
funnel gateway logs
|
|
5198
5206
|
funnel gateway logs -n 100`;
|
|
5199
5207
|
const logger = new NodeFunnelLogger();
|
|
5200
|
-
const tryParseJson
|
|
5208
|
+
const tryParseJson = (line) => {
|
|
5201
5209
|
try {
|
|
5202
5210
|
return JSON.parse(line);
|
|
5203
5211
|
} catch {
|
|
@@ -5243,7 +5251,7 @@ const gatewayLogsHandler = factory.createHandlers(zValidator$1("query", z.object
|
|
|
5243
5251
|
buffer = lines.pop() ?? "";
|
|
5244
5252
|
for (const line of lines) {
|
|
5245
5253
|
if (!line.trim()) continue;
|
|
5246
|
-
const parsed = tryParseJson
|
|
5254
|
+
const parsed = tryParseJson(line);
|
|
5247
5255
|
if (!isLogEntry(parsed)) {
|
|
5248
5256
|
process.stdout.write(`${line}\n`);
|
|
5249
5257
|
continue;
|
|
@@ -5737,2209 +5745,4 @@ const createCliApp = (funnel) => {
|
|
|
5737
5745
|
/** CLI Hono app wired to a default `new Funnel()`. For embedding with a custom Funnel use `createCliApp`. */
|
|
5738
5746
|
const app = createCliApp(new Funnel());
|
|
5739
5747
|
//#endregion
|
|
5740
|
-
|
|
5741
|
-
var tokens_default = { theme: { "extend": {
|
|
5742
|
-
"colors": {
|
|
5743
|
-
"background": "#09090b",
|
|
5744
|
-
"foreground": "#fafafa",
|
|
5745
|
-
"primary": "#fafafa",
|
|
5746
|
-
"primary-foreground": "#09090b",
|
|
5747
|
-
"primary-hover": "#e4e4e7",
|
|
5748
|
-
"primary-active": "#d4d4d8",
|
|
5749
|
-
"secondary": "#27272a",
|
|
5750
|
-
"secondary-foreground": "#fafafa",
|
|
5751
|
-
"secondary-hover": "#3f3f46",
|
|
5752
|
-
"secondary-active": "#52525b",
|
|
5753
|
-
"card": "#27272a",
|
|
5754
|
-
"card-foreground": "#fafafa",
|
|
5755
|
-
"popover": "#3f3f46",
|
|
5756
|
-
"popover-foreground": "#fafafa",
|
|
5757
|
-
"muted": "#27272a",
|
|
5758
|
-
"muted-foreground": "#a1a1aa",
|
|
5759
|
-
"accent": "#27272a",
|
|
5760
|
-
"accent-foreground": "#fafafa",
|
|
5761
|
-
"accent-hover": "#18181b",
|
|
5762
|
-
"accent-active": "#3f3f46",
|
|
5763
|
-
"destructive": "#7f1d1d",
|
|
5764
|
-
"destructive-foreground": "#fef2f2",
|
|
5765
|
-
"destructive-hover": "#991b1b",
|
|
5766
|
-
"destructive-active": "#b91c1c",
|
|
5767
|
-
"border": "#3f3f46",
|
|
5768
|
-
"input": "#3f3f46",
|
|
5769
|
-
"ring": "#d4d4d8",
|
|
5770
|
-
"hover-active": "#71717a"
|
|
5771
|
-
},
|
|
5772
|
-
"fontFamily": {},
|
|
5773
|
-
"fontSize": {},
|
|
5774
|
-
"borderRadius": {
|
|
5775
|
-
"sm": "0px",
|
|
5776
|
-
"md": "0px",
|
|
5777
|
-
"lg": "0px"
|
|
5778
|
-
},
|
|
5779
|
-
"spacing": {
|
|
5780
|
-
"xs": "1px",
|
|
5781
|
-
"sm": "2px",
|
|
5782
|
-
"md": "4px",
|
|
5783
|
-
"lg": "8px"
|
|
5784
|
-
}
|
|
5785
|
-
} } };
|
|
5786
|
-
//#endregion
|
|
5787
|
-
//#region lib/tui/utils/hascii/theme.ts
|
|
5788
|
-
const hasciiTw = { colors: {
|
|
5789
|
-
neutral: {
|
|
5790
|
-
50: "#fafafa",
|
|
5791
|
-
100: "#f5f5f5",
|
|
5792
|
-
200: "#e5e5e5",
|
|
5793
|
-
300: "#d4d4d4",
|
|
5794
|
-
400: "#a3a3a3",
|
|
5795
|
-
500: "#737373",
|
|
5796
|
-
600: "#525252",
|
|
5797
|
-
700: "#404040",
|
|
5798
|
-
800: "#262626",
|
|
5799
|
-
900: "#171717",
|
|
5800
|
-
950: "#0a0a0a"
|
|
5801
|
-
},
|
|
5802
|
-
zinc: {
|
|
5803
|
-
50: "#fafafa",
|
|
5804
|
-
100: "#f4f4f5",
|
|
5805
|
-
200: "#e4e4e7",
|
|
5806
|
-
300: "#d4d4d8",
|
|
5807
|
-
400: "#a1a1aa",
|
|
5808
|
-
500: "#71717a",
|
|
5809
|
-
600: "#52525b",
|
|
5810
|
-
700: "#3f3f46",
|
|
5811
|
-
800: "#27272a",
|
|
5812
|
-
900: "#18181b",
|
|
5813
|
-
950: "#09090b"
|
|
5814
|
-
},
|
|
5815
|
-
red: {
|
|
5816
|
-
50: "#fef2f2",
|
|
5817
|
-
100: "#fee2e2",
|
|
5818
|
-
200: "#fecaca",
|
|
5819
|
-
300: "#fca5a5",
|
|
5820
|
-
400: "#f87171",
|
|
5821
|
-
500: "#ef4444",
|
|
5822
|
-
600: "#dc2626",
|
|
5823
|
-
700: "#b91c1c",
|
|
5824
|
-
800: "#991b1b",
|
|
5825
|
-
900: "#7f1d1d",
|
|
5826
|
-
950: "#450a0a"
|
|
5827
|
-
}
|
|
5828
|
-
} };
|
|
5829
|
-
const HEX = /^#[0-9a-fA-F]{6}$/;
|
|
5830
|
-
const c = z.object({ theme: z.object({ extend: z.object({ colors: z.object({
|
|
5831
|
-
background: z.string().regex(HEX),
|
|
5832
|
-
foreground: z.string().regex(HEX),
|
|
5833
|
-
primary: z.string().regex(HEX),
|
|
5834
|
-
"primary-foreground": z.string().regex(HEX),
|
|
5835
|
-
"primary-hover": z.string().regex(HEX),
|
|
5836
|
-
"primary-active": z.string().regex(HEX),
|
|
5837
|
-
secondary: z.string().regex(HEX),
|
|
5838
|
-
"secondary-foreground": z.string().regex(HEX),
|
|
5839
|
-
"secondary-hover": z.string().regex(HEX),
|
|
5840
|
-
"secondary-active": z.string().regex(HEX),
|
|
5841
|
-
card: z.string().regex(HEX),
|
|
5842
|
-
"card-foreground": z.string().regex(HEX),
|
|
5843
|
-
popover: z.string().regex(HEX),
|
|
5844
|
-
"popover-foreground": z.string().regex(HEX),
|
|
5845
|
-
muted: z.string().regex(HEX),
|
|
5846
|
-
"muted-foreground": z.string().regex(HEX),
|
|
5847
|
-
accent: z.string().regex(HEX),
|
|
5848
|
-
"accent-foreground": z.string().regex(HEX),
|
|
5849
|
-
"accent-hover": z.string().regex(HEX),
|
|
5850
|
-
"accent-active": z.string().regex(HEX),
|
|
5851
|
-
destructive: z.string().regex(HEX),
|
|
5852
|
-
"destructive-foreground": z.string().regex(HEX),
|
|
5853
|
-
"destructive-hover": z.string().regex(HEX),
|
|
5854
|
-
"destructive-active": z.string().regex(HEX),
|
|
5855
|
-
border: z.string().regex(HEX),
|
|
5856
|
-
input: z.string().regex(HEX),
|
|
5857
|
-
ring: z.string().regex(HEX),
|
|
5858
|
-
"hover-active": z.string().regex(HEX)
|
|
5859
|
-
}) }) }) }).parse(tokens_default).theme.extend.colors;
|
|
5860
|
-
/** Default dark theme. Tokens are loaded from tokens.json (generated from DESIGN.md by `make tokens`) and validated with zod. */
|
|
5861
|
-
const hasciiTheme = { color: {
|
|
5862
|
-
background: c.background,
|
|
5863
|
-
foreground: c.foreground,
|
|
5864
|
-
primary: c.primary,
|
|
5865
|
-
primaryForeground: c["primary-foreground"],
|
|
5866
|
-
primaryHover: c["primary-hover"],
|
|
5867
|
-
primaryActive: c["primary-active"],
|
|
5868
|
-
secondary: c.secondary,
|
|
5869
|
-
secondaryForeground: c["secondary-foreground"],
|
|
5870
|
-
secondaryHover: c["secondary-hover"],
|
|
5871
|
-
secondaryActive: c["secondary-active"],
|
|
5872
|
-
card: c.card,
|
|
5873
|
-
cardForeground: c["card-foreground"],
|
|
5874
|
-
popover: c.popover,
|
|
5875
|
-
popoverForeground: c["popover-foreground"],
|
|
5876
|
-
muted: c.muted,
|
|
5877
|
-
mutedForeground: c["muted-foreground"],
|
|
5878
|
-
accent: c.accent,
|
|
5879
|
-
accentForeground: c["accent-foreground"],
|
|
5880
|
-
accentHover: c["accent-hover"],
|
|
5881
|
-
accentActive: c["accent-active"],
|
|
5882
|
-
destructive: c.destructive,
|
|
5883
|
-
destructiveForeground: c["destructive-foreground"],
|
|
5884
|
-
destructiveHover: c["destructive-hover"],
|
|
5885
|
-
destructiveActive: c["destructive-active"],
|
|
5886
|
-
border: c.border,
|
|
5887
|
-
input: c.input,
|
|
5888
|
-
ring: c.ring,
|
|
5889
|
-
hoverActive: c["hover-active"]
|
|
5890
|
-
} };
|
|
5891
|
-
//#endregion
|
|
5892
|
-
//#region lib/tui/theme.ts
|
|
5893
|
-
/**
|
|
5894
|
-
* Funnel-specific TUI tokens that hascii does not cover.
|
|
5895
|
-
*
|
|
5896
|
-
* Generic colors (background / foreground / primary / muted / etc.)
|
|
5897
|
-
* come from hascii via `useHasciiTheme()` — components that need them
|
|
5898
|
-
* read the theme there. Only the funnel-only concerns live here:
|
|
5899
|
-
*
|
|
5900
|
-
* - status accents (`alive` / `dead` / `warn`) and the selection
|
|
5901
|
-
* accent (`primary` blue) — semantic colors not in hascii's palette
|
|
5902
|
-
* - the in-between background tier `surface` (zinc[900]) sitting
|
|
5903
|
-
* between hascii's `background` and `muted`
|
|
5904
|
-
* - the deeper text tier `faint` (zinc[600]) below `mutedForeground`
|
|
5905
|
-
* - layout constants (paddingX / paddingY / gap / sidebarWidth / ...)
|
|
5906
|
-
*/
|
|
5907
|
-
const funnel = {
|
|
5908
|
-
alive: "#86efac",
|
|
5909
|
-
dead: "#fca5a5",
|
|
5910
|
-
warn: "#fcd34d",
|
|
5911
|
-
primary: "#3b82f6",
|
|
5912
|
-
surface: hasciiTw.colors.zinc[900],
|
|
5913
|
-
faint: hasciiTw.colors.zinc[600],
|
|
5914
|
-
paddingX: 2,
|
|
5915
|
-
paddingY: 1,
|
|
5916
|
-
gap: 1,
|
|
5917
|
-
sidebarWidth: 24,
|
|
5918
|
-
modalTop: 4,
|
|
5919
|
-
modalInset: "20%",
|
|
5920
|
-
barHeight: 1,
|
|
5921
|
-
detailPanelHeight: 14
|
|
5922
|
-
};
|
|
5923
|
-
//#endregion
|
|
5924
|
-
//#region lib/tui/utils/hascii/theme-context.tsx
|
|
5925
|
-
/** @jsxImportSource @opentui/react */
|
|
5926
|
-
const HasciiThemeContext = createContext(hasciiTheme);
|
|
5927
|
-
/** Read the active theme from context. Falls back to the default theme outside a provider. */
|
|
5928
|
-
function useHasciiTheme() {
|
|
5929
|
-
return useContext(HasciiThemeContext);
|
|
5930
|
-
}
|
|
5931
|
-
/** Wraps a subtree with a HasciiTheme so descendants can read tokens via useHasciiTheme. */
|
|
5932
|
-
function HasciiThemeProvider(props) {
|
|
5933
|
-
return /* @__PURE__ */ jsx(HasciiThemeContext.Provider, {
|
|
5934
|
-
value: props.theme ?? hasciiTheme,
|
|
5935
|
-
children: props.children
|
|
5936
|
-
});
|
|
5937
|
-
}
|
|
5938
|
-
//#endregion
|
|
5939
|
-
//#region lib/tui/filter-input.tsx
|
|
5940
|
-
/** @jsxImportSource @opentui/react */
|
|
5941
|
-
/** Inline filter overlay shown when the user presses `/`. */
|
|
5942
|
-
function FilterInput(props) {
|
|
5943
|
-
const theme = useHasciiTheme();
|
|
5944
|
-
if (!props.active) return null;
|
|
5945
|
-
return /* @__PURE__ */ jsx("box", {
|
|
5946
|
-
style: {
|
|
5947
|
-
height: funnel.barHeight,
|
|
5948
|
-
backgroundColor: theme.color.muted,
|
|
5949
|
-
paddingLeft: funnel.paddingX,
|
|
5950
|
-
paddingRight: funnel.paddingX
|
|
5951
|
-
},
|
|
5952
|
-
children: /* @__PURE__ */ jsxs("text", { children: [
|
|
5953
|
-
/* @__PURE__ */ jsx("span", {
|
|
5954
|
-
fg: theme.color.foreground,
|
|
5955
|
-
children: "/"
|
|
5956
|
-
}),
|
|
5957
|
-
/* @__PURE__ */ jsx("span", {
|
|
5958
|
-
fg: theme.color.foreground,
|
|
5959
|
-
children: props.value
|
|
5960
|
-
}),
|
|
5961
|
-
/* @__PURE__ */ jsx("span", {
|
|
5962
|
-
fg: theme.color.foreground,
|
|
5963
|
-
children: "█"
|
|
5964
|
-
}),
|
|
5965
|
-
/* @__PURE__ */ jsx("span", {
|
|
5966
|
-
fg: theme.color.mutedForeground,
|
|
5967
|
-
children: " · Enter to apply · Esc to cancel"
|
|
5968
|
-
})
|
|
5969
|
-
] })
|
|
5970
|
-
});
|
|
5971
|
-
}
|
|
5972
|
-
//#endregion
|
|
5973
|
-
//#region lib/tui/components/ui/hascii/separator.tsx
|
|
5974
|
-
/** Hairline divider drawn with box-drawing characters. Length is the run in the chosen orientation. */
|
|
5975
|
-
function HasciiSeparator(props) {
|
|
5976
|
-
const orientation = props.orientation ?? "horizontal";
|
|
5977
|
-
const length = props.length ?? (orientation === "horizontal" ? 32 : 8);
|
|
5978
|
-
const theme = useHasciiTheme();
|
|
5979
|
-
const color = props.color ?? theme.color.border;
|
|
5980
|
-
if (orientation === "vertical") {
|
|
5981
|
-
const lines = [];
|
|
5982
|
-
for (let index = 0; index < length; index++) lines.push("│");
|
|
5983
|
-
return /* @__PURE__ */ jsx("box", {
|
|
5984
|
-
width: 1,
|
|
5985
|
-
height: length,
|
|
5986
|
-
flexShrink: 0,
|
|
5987
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
5988
|
-
fg: color,
|
|
5989
|
-
children: lines.join("\n")
|
|
5990
|
-
})
|
|
5991
|
-
});
|
|
5992
|
-
}
|
|
5993
|
-
return /* @__PURE__ */ jsx("box", {
|
|
5994
|
-
width: length,
|
|
5995
|
-
height: 1,
|
|
5996
|
-
flexShrink: 0,
|
|
5997
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
5998
|
-
fg: color,
|
|
5999
|
-
children: "─".repeat(length)
|
|
6000
|
-
})
|
|
6001
|
-
});
|
|
6002
|
-
}
|
|
6003
|
-
//#endregion
|
|
6004
|
-
//#region lib/tui/components/empty-state.tsx
|
|
6005
|
-
/** @jsxImportSource @opentui/react */
|
|
6006
|
-
/** Faint placeholder shown when a list has no items. */
|
|
6007
|
-
function EmptyState(props) {
|
|
6008
|
-
return /* @__PURE__ */ jsx("text", {
|
|
6009
|
-
fg: funnel.faint,
|
|
6010
|
-
children: props.message
|
|
6011
|
-
});
|
|
6012
|
-
}
|
|
6013
|
-
//#endregion
|
|
6014
|
-
//#region lib/tui/profile-launcher.tsx
|
|
6015
|
-
/** @jsxImportSource @opentui/react */
|
|
6016
|
-
/**
|
|
6017
|
-
* Modal-style overlay: pick a profile and launch Claude Code via the same
|
|
6018
|
-
* code path as `fnl claude --profile`. The launcher exits the TUI before
|
|
6019
|
-
* exec'ing so Claude takes over the terminal.
|
|
6020
|
-
*/
|
|
6021
|
-
function ProfileLauncher(props) {
|
|
6022
|
-
const theme = useHasciiTheme();
|
|
6023
|
-
if (!props.active) return null;
|
|
6024
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
6025
|
-
style: {
|
|
6026
|
-
flexDirection: "column",
|
|
6027
|
-
backgroundColor: funnel.surface,
|
|
6028
|
-
paddingLeft: funnel.paddingX,
|
|
6029
|
-
paddingRight: funnel.paddingX,
|
|
6030
|
-
paddingTop: funnel.paddingY,
|
|
6031
|
-
paddingBottom: funnel.paddingY,
|
|
6032
|
-
gap: funnel.gap,
|
|
6033
|
-
position: "absolute",
|
|
6034
|
-
top: funnel.modalTop,
|
|
6035
|
-
left: funnel.modalInset,
|
|
6036
|
-
right: funnel.modalInset
|
|
6037
|
-
},
|
|
6038
|
-
children: [
|
|
6039
|
-
/* @__PURE__ */ jsx("text", {
|
|
6040
|
-
fg: theme.color.foreground,
|
|
6041
|
-
children: "launch claude with profile"
|
|
6042
|
-
}),
|
|
6043
|
-
/* @__PURE__ */ jsx(HasciiSeparator, {}),
|
|
6044
|
-
props.profiles.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "(no profiles — `fnl profiles add` first)" }) : props.profiles.map((profile, index) => {
|
|
6045
|
-
return /* @__PURE__ */ jsxs("text", {
|
|
6046
|
-
bg: index === props.selectedIndex ? theme.color.muted : void 0,
|
|
6047
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
6048
|
-
fg: theme.color.foreground,
|
|
6049
|
-
children: profile.name
|
|
6050
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
6051
|
-
fg: funnel.faint,
|
|
6052
|
-
children: ` → channel ${profile.channelId} · path ${profile.path}`
|
|
6053
|
-
})]
|
|
6054
|
-
}, profile.name);
|
|
6055
|
-
})
|
|
6056
|
-
]
|
|
6057
|
-
});
|
|
6058
|
-
}
|
|
6059
|
-
//#endregion
|
|
6060
|
-
//#region lib/tui/components/brand.tsx
|
|
6061
|
-
/** @jsxImportSource @opentui/react */
|
|
6062
|
-
/**
|
|
6063
|
-
* Brand mark rendered at the top of the sidebar. Carries its own paddingX
|
|
6064
|
-
* so it aligns with menu items (which need paddingX-less parents so their
|
|
6065
|
-
* active highlight spans edge-to-edge).
|
|
6066
|
-
*/
|
|
6067
|
-
function Brand() {
|
|
6068
|
-
const theme = useHasciiTheme();
|
|
6069
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6070
|
-
style: {
|
|
6071
|
-
flexDirection: "row",
|
|
6072
|
-
paddingLeft: funnel.paddingX,
|
|
6073
|
-
paddingRight: funnel.paddingX
|
|
6074
|
-
},
|
|
6075
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6076
|
-
fg: theme.color.foreground,
|
|
6077
|
-
attributes: TextAttributes.BOLD,
|
|
6078
|
-
children: "funnel"
|
|
6079
|
-
})
|
|
6080
|
-
});
|
|
6081
|
-
}
|
|
6082
|
-
//#endregion
|
|
6083
|
-
//#region lib/tui/components/ui/hascii/focus-group.tsx
|
|
6084
|
-
const HasciiFocusContext = createContext(null);
|
|
6085
|
-
/** Returns whether the provided focusId matches the surrounding HasciiFocusGroup's current focus. */
|
|
6086
|
-
function useHasciiFocus(focusId) {
|
|
6087
|
-
const ctx = useContext(HasciiFocusContext);
|
|
6088
|
-
if (!ctx || focusId === void 0) return false;
|
|
6089
|
-
return ctx.currentId === focusId;
|
|
6090
|
-
}
|
|
6091
|
-
//#endregion
|
|
6092
|
-
//#region lib/tui/hooks/hascii/use-pressable.ts
|
|
6093
|
-
/** Tracks hover and press state for a focusable element and exposes mouse handlers ready for spread. */
|
|
6094
|
-
function usePressable(options) {
|
|
6095
|
-
const isDisabled = options?.isDisabled ?? false;
|
|
6096
|
-
const onPress = options?.onPress;
|
|
6097
|
-
const hoveredState = useState(false);
|
|
6098
|
-
const isHovered = hoveredState[0];
|
|
6099
|
-
const setHovered = hoveredState[1];
|
|
6100
|
-
const pressedState = useState(false);
|
|
6101
|
-
const isPressed = pressedState[0];
|
|
6102
|
-
const setPressed = pressedState[1];
|
|
6103
|
-
return {
|
|
6104
|
-
isHovered,
|
|
6105
|
-
isPressed,
|
|
6106
|
-
bind: {
|
|
6107
|
-
onMouseOver: () => {
|
|
6108
|
-
if (!isDisabled) setHovered(true);
|
|
6109
|
-
},
|
|
6110
|
-
onMouseOut: () => {
|
|
6111
|
-
setHovered(false);
|
|
6112
|
-
setPressed(false);
|
|
6113
|
-
},
|
|
6114
|
-
onMouseDown: () => {
|
|
6115
|
-
if (!isDisabled) setPressed(true);
|
|
6116
|
-
},
|
|
6117
|
-
onMouseUp: () => {
|
|
6118
|
-
if (isDisabled) return;
|
|
6119
|
-
if (isPressed) onPress?.();
|
|
6120
|
-
setPressed(false);
|
|
6121
|
-
}
|
|
6122
|
-
}
|
|
6123
|
-
};
|
|
6124
|
-
}
|
|
6125
|
-
//#endregion
|
|
6126
|
-
//#region lib/tui/components/ui/hascii/button.tsx
|
|
6127
|
-
const sizeDims = {
|
|
6128
|
-
sm: {
|
|
6129
|
-
paddingX: 1,
|
|
6130
|
-
height: 1
|
|
6131
|
-
},
|
|
6132
|
-
md: {
|
|
6133
|
-
paddingX: 2,
|
|
6134
|
-
height: 1
|
|
6135
|
-
},
|
|
6136
|
-
lg: {
|
|
6137
|
-
paddingX: 2,
|
|
6138
|
-
height: 3
|
|
6139
|
-
}
|
|
6140
|
-
};
|
|
6141
|
-
const resolveSize = (size) => size === "default" ? "md" : size;
|
|
6142
|
-
const pickBg = (rest, hover, active, isHover, isActive) => {
|
|
6143
|
-
if (isActive) return active;
|
|
6144
|
-
if (isHover) return hover;
|
|
6145
|
-
return rest;
|
|
6146
|
-
};
|
|
6147
|
-
/** A focusable terminal button. Background cycles through rest, hover, and active states. */
|
|
6148
|
-
function HasciiButton(props) {
|
|
6149
|
-
const variant = props.variant ?? "default";
|
|
6150
|
-
const size = props.size ?? "default";
|
|
6151
|
-
const groupFocused = useHasciiFocus(props.focusId);
|
|
6152
|
-
const isFocused = props.isFocused ?? groupFocused;
|
|
6153
|
-
const isDisabled = props.isDisabled ?? false;
|
|
6154
|
-
const theme = useHasciiTheme();
|
|
6155
|
-
const resolvedSize = resolveSize(size);
|
|
6156
|
-
const dims = sizeDims[resolvedSize];
|
|
6157
|
-
const press = usePressable({
|
|
6158
|
-
isDisabled,
|
|
6159
|
-
onPress: props.onPress
|
|
6160
|
-
});
|
|
6161
|
-
const flashState = useState(false);
|
|
6162
|
-
const flashed = flashState[0];
|
|
6163
|
-
const setFlashed = flashState[1];
|
|
6164
|
-
const isHover = press.isHovered && !press.isPressed && !flashed;
|
|
6165
|
-
const isActive = press.isPressed || flashed;
|
|
6166
|
-
useKeyboard((key) => {
|
|
6167
|
-
if (!isFocused || isDisabled) return;
|
|
6168
|
-
if (key.name === "return" || key.name === "space") {
|
|
6169
|
-
setFlashed(true);
|
|
6170
|
-
props.onPress?.();
|
|
6171
|
-
setTimeout(() => setFlashed(false), 120);
|
|
6172
|
-
}
|
|
6173
|
-
});
|
|
6174
|
-
if (variant === "outline") {
|
|
6175
|
-
const tone = isDisabled ? theme.color.border : isActive ? theme.color.primaryActive : isHover ? theme.color.primaryHover : theme.color.primary;
|
|
6176
|
-
const outlinePaddingX = resolvedSize === "sm" ? 0 : resolvedSize === "md" ? 1 : 2;
|
|
6177
|
-
const outlineHeight = 3;
|
|
6178
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6179
|
-
paddingLeft: outlinePaddingX,
|
|
6180
|
-
paddingRight: outlinePaddingX,
|
|
6181
|
-
height: outlineHeight,
|
|
6182
|
-
border: outlineHeight >= 3,
|
|
6183
|
-
borderStyle: "rounded",
|
|
6184
|
-
borderColor: tone,
|
|
6185
|
-
alignItems: "center",
|
|
6186
|
-
justifyContent: "center",
|
|
6187
|
-
...press.bind,
|
|
6188
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6189
|
-
fg: tone,
|
|
6190
|
-
children: props.children
|
|
6191
|
-
})
|
|
6192
|
-
});
|
|
6193
|
-
}
|
|
6194
|
-
if (variant === "ghost") {
|
|
6195
|
-
const fg = isDisabled ? theme.color.mutedForeground : theme.color.foreground;
|
|
6196
|
-
const bg = pickBg(void 0, theme.color.accentHover, theme.color.accent, isHover, isActive);
|
|
6197
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6198
|
-
paddingLeft: dims.paddingX,
|
|
6199
|
-
paddingRight: dims.paddingX,
|
|
6200
|
-
height: dims.height,
|
|
6201
|
-
backgroundColor: bg,
|
|
6202
|
-
alignItems: "center",
|
|
6203
|
-
justifyContent: "center",
|
|
6204
|
-
...press.bind,
|
|
6205
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6206
|
-
fg,
|
|
6207
|
-
children: props.children
|
|
6208
|
-
})
|
|
6209
|
-
});
|
|
6210
|
-
}
|
|
6211
|
-
if (variant === "secondary") {
|
|
6212
|
-
const fg = isDisabled ? theme.color.mutedForeground : theme.color.secondaryForeground;
|
|
6213
|
-
const bg = pickBg(theme.color.secondary, theme.color.secondaryHover, theme.color.secondaryActive, isHover, isActive);
|
|
6214
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6215
|
-
paddingLeft: dims.paddingX,
|
|
6216
|
-
paddingRight: dims.paddingX,
|
|
6217
|
-
height: dims.height,
|
|
6218
|
-
backgroundColor: bg,
|
|
6219
|
-
alignItems: "center",
|
|
6220
|
-
justifyContent: "center",
|
|
6221
|
-
...press.bind,
|
|
6222
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6223
|
-
fg,
|
|
6224
|
-
children: props.children
|
|
6225
|
-
})
|
|
6226
|
-
});
|
|
6227
|
-
}
|
|
6228
|
-
if (variant === "destructive") {
|
|
6229
|
-
const fg = isDisabled ? theme.color.mutedForeground : theme.color.destructiveForeground;
|
|
6230
|
-
const bg = pickBg(theme.color.destructive, theme.color.destructiveHover, theme.color.destructiveActive, isHover, isActive);
|
|
6231
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6232
|
-
paddingLeft: dims.paddingX,
|
|
6233
|
-
paddingRight: dims.paddingX,
|
|
6234
|
-
height: dims.height,
|
|
6235
|
-
backgroundColor: bg,
|
|
6236
|
-
alignItems: "center",
|
|
6237
|
-
justifyContent: "center",
|
|
6238
|
-
...press.bind,
|
|
6239
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6240
|
-
fg,
|
|
6241
|
-
children: props.children
|
|
6242
|
-
})
|
|
6243
|
-
});
|
|
6244
|
-
}
|
|
6245
|
-
const fg = isDisabled ? theme.color.mutedForeground : theme.color.primaryForeground;
|
|
6246
|
-
const bg = pickBg(theme.color.primary, theme.color.primaryHover, theme.color.primaryActive, isHover, isActive);
|
|
6247
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6248
|
-
paddingLeft: dims.paddingX,
|
|
6249
|
-
paddingRight: dims.paddingX,
|
|
6250
|
-
height: dims.height,
|
|
6251
|
-
backgroundColor: bg,
|
|
6252
|
-
alignItems: "center",
|
|
6253
|
-
justifyContent: "center",
|
|
6254
|
-
...press.bind,
|
|
6255
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6256
|
-
fg,
|
|
6257
|
-
children: props.children
|
|
6258
|
-
})
|
|
6259
|
-
});
|
|
6260
|
-
}
|
|
6261
|
-
//#endregion
|
|
6262
|
-
//#region lib/tui/components/gateway-status.tsx
|
|
6263
|
-
/** @jsxImportSource @opentui/react */
|
|
6264
|
-
/**
|
|
6265
|
-
* Compact running/stopped indicator with pid and port for the sidebar.
|
|
6266
|
-
*
|
|
6267
|
-
* The "gateway" label lives inside the same elevated block as the status
|
|
6268
|
-
* so the heading reads as part of the card, not a floating sidebar
|
|
6269
|
-
* separator on the surrounding surface tier.
|
|
6270
|
-
*
|
|
6271
|
-
* The toggle is rendered as a neutral-white `Button` with the same
|
|
6272
|
-
* paddingX=2 / paddingY=1 as the rest of the form chrome — no leading
|
|
6273
|
-
* glyph, just the verb. `busy` disables the button while a toggle is
|
|
6274
|
-
* in flight so rapid clicks don't stack daemon spawns.
|
|
6275
|
-
*/
|
|
6276
|
-
function GatewayStatus(props) {
|
|
6277
|
-
const theme = useHasciiTheme();
|
|
6278
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
6279
|
-
style: {
|
|
6280
|
-
flexDirection: "column",
|
|
6281
|
-
backgroundColor: theme.color.muted,
|
|
6282
|
-
paddingLeft: funnel.paddingX,
|
|
6283
|
-
paddingRight: funnel.paddingX,
|
|
6284
|
-
paddingTop: funnel.paddingY,
|
|
6285
|
-
paddingBottom: funnel.paddingY,
|
|
6286
|
-
gap: funnel.gap
|
|
6287
|
-
},
|
|
6288
|
-
children: [/* @__PURE__ */ jsx("text", {
|
|
6289
|
-
fg: funnel.faint,
|
|
6290
|
-
children: "gateway"
|
|
6291
|
-
}), props.gateway.running ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
6292
|
-
/* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
6293
|
-
fg: funnel.alive,
|
|
6294
|
-
children: "●"
|
|
6295
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
6296
|
-
fg: theme.color.foreground,
|
|
6297
|
-
children: ` running`
|
|
6298
|
-
})] }),
|
|
6299
|
-
/* @__PURE__ */ jsx("text", {
|
|
6300
|
-
fg: funnel.faint,
|
|
6301
|
-
children: `pid ${props.gateway.pid} · :${props.gateway.port}`
|
|
6302
|
-
}),
|
|
6303
|
-
/* @__PURE__ */ jsx(HasciiButton, {
|
|
6304
|
-
onPress: props.onToggle,
|
|
6305
|
-
isDisabled: props.busy,
|
|
6306
|
-
children: props.busy ? "stopping…" : "stop"
|
|
6307
|
-
})
|
|
6308
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
6309
|
-
fg: theme.color.mutedForeground,
|
|
6310
|
-
children: "○"
|
|
6311
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
6312
|
-
fg: theme.color.mutedForeground,
|
|
6313
|
-
children: ` stopped`
|
|
6314
|
-
})] }), /* @__PURE__ */ jsx(HasciiButton, {
|
|
6315
|
-
onPress: props.onToggle,
|
|
6316
|
-
isDisabled: props.busy,
|
|
6317
|
-
children: props.busy ? "starting…" : "start"
|
|
6318
|
-
})] })]
|
|
6319
|
-
});
|
|
6320
|
-
}
|
|
6321
|
-
//#endregion
|
|
6322
|
-
//#region lib/tui/components/menu-item.tsx
|
|
6323
|
-
/** @jsxImportSource @opentui/react */
|
|
6324
|
-
const ROW_HEIGHT = funnel.paddingY * 2 + 1;
|
|
6325
|
-
/**
|
|
6326
|
-
* One row in the sidebar nav.
|
|
6327
|
-
*
|
|
6328
|
-
* Active state: a thin `▏` (U+258F LEFT ONE EIGHTH BLOCK) rule painted
|
|
6329
|
-
* in the primary color, stacked to fill the row height. The rule is
|
|
6330
|
-
* narrower than a full character cell so it reads as a delicate accent
|
|
6331
|
-
* instead of a heavy block. The sidebar's own background is `muted`,
|
|
6332
|
-
* so hover/active lift the row to `secondaryHover` / `secondaryActive`
|
|
6333
|
-
* to read against it.
|
|
6334
|
-
*/
|
|
6335
|
-
function MenuItem(props) {
|
|
6336
|
-
const theme = useHasciiTheme();
|
|
6337
|
-
const press = usePressable({ onPress: props.onSelect });
|
|
6338
|
-
const bg = press.isPressed ? theme.color.secondaryActive : props.active || press.isHovered ? theme.color.secondaryHover : void 0;
|
|
6339
|
-
const isLifted = props.active || press.isHovered || press.isPressed;
|
|
6340
|
-
const fg = isLifted ? theme.color.foreground : theme.color.foreground;
|
|
6341
|
-
const countFg = isLifted ? theme.color.foreground : funnel.faint;
|
|
6342
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
6343
|
-
...press.bind,
|
|
6344
|
-
style: {
|
|
6345
|
-
flexDirection: "row",
|
|
6346
|
-
justifyContent: "space-between",
|
|
6347
|
-
backgroundColor: bg,
|
|
6348
|
-
paddingLeft: funnel.paddingX,
|
|
6349
|
-
paddingRight: funnel.paddingX,
|
|
6350
|
-
paddingTop: funnel.paddingY,
|
|
6351
|
-
paddingBottom: funnel.paddingY
|
|
6352
|
-
},
|
|
6353
|
-
children: [
|
|
6354
|
-
props.active ? /* @__PURE__ */ jsx("box", {
|
|
6355
|
-
style: {
|
|
6356
|
-
position: "absolute",
|
|
6357
|
-
left: 0,
|
|
6358
|
-
top: 0,
|
|
6359
|
-
bottom: 0,
|
|
6360
|
-
flexDirection: "column"
|
|
6361
|
-
},
|
|
6362
|
-
children: Array.from({ length: ROW_HEIGHT }, (_, index) => /* @__PURE__ */ jsx("text", {
|
|
6363
|
-
fg: funnel.primary,
|
|
6364
|
-
children: "▏"
|
|
6365
|
-
}, index))
|
|
6366
|
-
}) : null,
|
|
6367
|
-
/* @__PURE__ */ jsx("text", {
|
|
6368
|
-
fg,
|
|
6369
|
-
children: props.label
|
|
6370
|
-
}),
|
|
6371
|
-
props.count !== void 0 ? /* @__PURE__ */ jsx("text", {
|
|
6372
|
-
fg: countFg,
|
|
6373
|
-
children: String(props.count)
|
|
6374
|
-
}) : null
|
|
6375
|
-
]
|
|
6376
|
-
});
|
|
6377
|
-
}
|
|
6378
|
-
//#endregion
|
|
6379
|
-
//#region lib/tui/components/menu.tsx
|
|
6380
|
-
/** @jsxImportSource @opentui/react */
|
|
6381
|
-
/** Vertical list of clickable nav rows. Each row is a `MenuItem`. */
|
|
6382
|
-
function Menu(props) {
|
|
6383
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6384
|
-
style: { flexDirection: "column" },
|
|
6385
|
-
children: props.items.map((item) => /* @__PURE__ */ jsx(MenuItem, {
|
|
6386
|
-
label: item.label,
|
|
6387
|
-
active: item.view === props.active,
|
|
6388
|
-
count: item.count,
|
|
6389
|
-
onSelect: () => props.onSelect(item.view)
|
|
6390
|
-
}, item.view))
|
|
6391
|
-
});
|
|
6392
|
-
}
|
|
6393
|
-
//#endregion
|
|
6394
|
-
//#region lib/tui/components/section-header.tsx
|
|
6395
|
-
/** @jsxImportSource @opentui/react */
|
|
6396
|
-
/**
|
|
6397
|
-
* Tiny faint label rendered above each sidebar section. Wrapped in a box
|
|
6398
|
-
* because OpenTUI ignores padding on `<text>`; only boxes lay out with
|
|
6399
|
-
* padding.
|
|
6400
|
-
*/
|
|
6401
|
-
function SectionHeader(props) {
|
|
6402
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6403
|
-
style: {
|
|
6404
|
-
flexDirection: "row",
|
|
6405
|
-
paddingLeft: funnel.paddingX,
|
|
6406
|
-
paddingRight: funnel.paddingX
|
|
6407
|
-
},
|
|
6408
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6409
|
-
fg: funnel.faint,
|
|
6410
|
-
children: props.label
|
|
6411
|
-
})
|
|
6412
|
-
});
|
|
6413
|
-
}
|
|
6414
|
-
//#endregion
|
|
6415
|
-
//#region lib/tui/components/session-item.tsx
|
|
6416
|
-
/** @jsxImportSource @opentui/react */
|
|
6417
|
-
/** One connected WebSocket session — channel name + connector summary. */
|
|
6418
|
-
function SessionItem(props) {
|
|
6419
|
-
const theme = useHasciiTheme();
|
|
6420
|
-
const { session } = props;
|
|
6421
|
-
const summary = session.connectors.length === 0 ? "(no connectors)" : session.connectors.length === 1 ? session.connectors[0] : `${session.connectors.length} connectors`;
|
|
6422
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
6423
|
-
style: {
|
|
6424
|
-
flexDirection: "column",
|
|
6425
|
-
paddingLeft: funnel.paddingX,
|
|
6426
|
-
paddingRight: funnel.paddingX
|
|
6427
|
-
},
|
|
6428
|
-
children: [/* @__PURE__ */ jsx("text", {
|
|
6429
|
-
fg: theme.color.foreground,
|
|
6430
|
-
children: session.channel || "(unnamed)"
|
|
6431
|
-
}), /* @__PURE__ */ jsx("text", {
|
|
6432
|
-
fg: funnel.faint,
|
|
6433
|
-
children: summary
|
|
6434
|
-
})]
|
|
6435
|
-
});
|
|
6436
|
-
}
|
|
6437
|
-
//#endregion
|
|
6438
|
-
//#region lib/tui/components/session-list.tsx
|
|
6439
|
-
/** @jsxImportSource @opentui/react */
|
|
6440
|
-
/** Vertical list of connected sessions (Claude MCP clients) for the sidebar. */
|
|
6441
|
-
function SessionList(props) {
|
|
6442
|
-
if (props.sessions.length === 0) return /* @__PURE__ */ jsx("box", {
|
|
6443
|
-
style: {
|
|
6444
|
-
flexDirection: "row",
|
|
6445
|
-
paddingLeft: funnel.paddingX,
|
|
6446
|
-
paddingRight: funnel.paddingX
|
|
6447
|
-
},
|
|
6448
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6449
|
-
fg: funnel.faint,
|
|
6450
|
-
children: "(none)"
|
|
6451
|
-
})
|
|
6452
|
-
});
|
|
6453
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6454
|
-
style: { flexDirection: "column" },
|
|
6455
|
-
children: props.sessions.map((session, index) => /* @__PURE__ */ jsx(SessionItem, { session }, `${session.channel}-${index}`))
|
|
6456
|
-
});
|
|
6457
|
-
}
|
|
6458
|
-
//#endregion
|
|
6459
|
-
//#region lib/tui/components/ui/hascii/sidebar.tsx
|
|
6460
|
-
/** Fixed-width vertical sidebar. Compose with HasciiSidebarHeader, HasciiSidebarContent, HasciiSidebarMenuItem. */
|
|
6461
|
-
function HasciiSidebar(props) {
|
|
6462
|
-
const theme = useHasciiTheme();
|
|
6463
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6464
|
-
flexDirection: "column",
|
|
6465
|
-
width: props.width ?? 24,
|
|
6466
|
-
backgroundColor: theme.color.muted,
|
|
6467
|
-
paddingTop: 1,
|
|
6468
|
-
gap: 0,
|
|
6469
|
-
children: props.children
|
|
6470
|
-
});
|
|
6471
|
-
}
|
|
6472
|
-
//#endregion
|
|
6473
|
-
//#region lib/tui/components/ui/hascii/sidebar-header.tsx
|
|
6474
|
-
/** Top section of a HasciiSidebar. Renders children at the sidebar's text padding. */
|
|
6475
|
-
function HasciiSidebarHeader(props) {
|
|
6476
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6477
|
-
flexDirection: "column",
|
|
6478
|
-
paddingLeft: 2,
|
|
6479
|
-
paddingRight: 2,
|
|
6480
|
-
paddingBottom: 1,
|
|
6481
|
-
gap: 0,
|
|
6482
|
-
children: props.children
|
|
6483
|
-
});
|
|
6484
|
-
}
|
|
6485
|
-
//#endregion
|
|
6486
|
-
//#region lib/tui/sidebar.tsx
|
|
6487
|
-
/** @jsxImportSource @opentui/react */
|
|
6488
|
-
/**
|
|
6489
|
-
* Left rail built on hascii's HasciiSidebar shell.
|
|
6490
|
-
*
|
|
6491
|
-
* Sections (top → bottom): brand (header slot), gateway card, sessions,
|
|
6492
|
-
* navigation menu. The funnel `Menu` items keep the left-edge `▏`
|
|
6493
|
-
* primary accent and right-aligned counts that hascii's stock sidebar
|
|
6494
|
-
* menu does not provide.
|
|
6495
|
-
*/
|
|
6496
|
-
function Sidebar(props) {
|
|
6497
|
-
return /* @__PURE__ */ jsxs(HasciiSidebar, {
|
|
6498
|
-
width: funnel.sidebarWidth,
|
|
6499
|
-
children: [
|
|
6500
|
-
/* @__PURE__ */ jsx(HasciiSidebarHeader, { children: /* @__PURE__ */ jsx(Brand, {}) }),
|
|
6501
|
-
/* @__PURE__ */ jsx(GatewayStatus, {
|
|
6502
|
-
gateway: props.snapshot.gateway,
|
|
6503
|
-
busy: props.busy,
|
|
6504
|
-
onToggle: props.onToggleGateway
|
|
6505
|
-
}),
|
|
6506
|
-
/* @__PURE__ */ jsxs("box", {
|
|
6507
|
-
style: { flexDirection: "column" },
|
|
6508
|
-
children: [/* @__PURE__ */ jsx(SectionHeader, { label: "sessions" }), /* @__PURE__ */ jsx(SessionList, { sessions: props.snapshot.sessions })]
|
|
6509
|
-
}),
|
|
6510
|
-
/* @__PURE__ */ jsx(Menu, {
|
|
6511
|
-
items: props.menuItems,
|
|
6512
|
-
active: props.view,
|
|
6513
|
-
onSelect: props.onSelect
|
|
6514
|
-
})
|
|
6515
|
-
]
|
|
6516
|
-
});
|
|
6517
|
-
}
|
|
6518
|
-
//#endregion
|
|
6519
|
-
//#region lib/tui/use-event-stream.ts
|
|
6520
|
-
const MAX_BUFFER = 200;
|
|
6521
|
-
const RECONNECT_BASE_MS = 500;
|
|
6522
|
-
const RECONNECT_MAX_MS = 8e3;
|
|
6523
|
-
const eventPayloadSchema = z.object({
|
|
6524
|
-
content: z.string(),
|
|
6525
|
-
meta: z.record(z.string(), z.string()).optional(),
|
|
6526
|
-
offset: z.number().int().nonnegative().optional()
|
|
6527
|
-
});
|
|
6528
|
-
/**
|
|
6529
|
-
* Opens a `tap=all` WebSocket against the gateway daemon and accumulates
|
|
6530
|
-
* received events in a ring buffer. Reconnects with capped exponential
|
|
6531
|
-
* backoff. Returns `disabled` status until the daemon comes online.
|
|
6532
|
-
*
|
|
6533
|
-
* `token` is appended as `?token=` so the gateway accepts the upgrade.
|
|
6534
|
-
*/
|
|
6535
|
-
const useEventStream = (port, daemonReachable, token) => {
|
|
6536
|
-
const [events, setEvents] = useState([]);
|
|
6537
|
-
const [status, setStatus] = useState("disabled");
|
|
6538
|
-
const [resetTick, setResetTick] = useState(0);
|
|
6539
|
-
useEffect(() => {
|
|
6540
|
-
if (!daemonReachable) {
|
|
6541
|
-
setStatus("disabled");
|
|
6542
|
-
return;
|
|
6543
|
-
}
|
|
6544
|
-
let cancelled = false;
|
|
6545
|
-
let socket = null;
|
|
6546
|
-
let reconnectTimer = null;
|
|
6547
|
-
let attempt = 0;
|
|
6548
|
-
let nextId = events.length > 0 ? Math.max(...events.map((e) => e.id)) + 1 : 1;
|
|
6549
|
-
let lastOffset = 0;
|
|
6550
|
-
const connect = () => {
|
|
6551
|
-
if (cancelled) return;
|
|
6552
|
-
setStatus("connecting");
|
|
6553
|
-
const sinceQuery = lastOffset > 0 ? `&since=${lastOffset}` : "";
|
|
6554
|
-
const protocols = token ? [`funnel.token.${token}`] : void 0;
|
|
6555
|
-
socket = new WebSocket(`ws://localhost:${port}/ws?tap=all${sinceQuery}`, protocols);
|
|
6556
|
-
socket.addEventListener("open", () => {
|
|
6557
|
-
if (cancelled) return;
|
|
6558
|
-
attempt = 0;
|
|
6559
|
-
setStatus("open");
|
|
6560
|
-
});
|
|
6561
|
-
socket.addEventListener("message", (event) => {
|
|
6562
|
-
if (cancelled) return;
|
|
6563
|
-
const raw = (() => {
|
|
6564
|
-
try {
|
|
6565
|
-
return JSON.parse(String(event.data));
|
|
6566
|
-
} catch {
|
|
6567
|
-
return null;
|
|
6568
|
-
}
|
|
6569
|
-
})();
|
|
6570
|
-
const parsed = eventPayloadSchema.safeParse(raw);
|
|
6571
|
-
if (!parsed.success) return;
|
|
6572
|
-
if (typeof parsed.data.offset === "number") lastOffset = parsed.data.offset;
|
|
6573
|
-
const next = {
|
|
6574
|
-
id: nextId,
|
|
6575
|
-
receivedAt: Date.now(),
|
|
6576
|
-
content: parsed.data.content,
|
|
6577
|
-
meta: parsed.data.meta ?? {}
|
|
6578
|
-
};
|
|
6579
|
-
nextId += 1;
|
|
6580
|
-
setEvents((prev) => {
|
|
6581
|
-
const merged = [next, ...prev];
|
|
6582
|
-
return merged.length > MAX_BUFFER ? merged.slice(0, MAX_BUFFER) : merged;
|
|
6583
|
-
});
|
|
6584
|
-
});
|
|
6585
|
-
socket.addEventListener("close", () => {
|
|
6586
|
-
if (cancelled) return;
|
|
6587
|
-
setStatus("closed");
|
|
6588
|
-
attempt += 1;
|
|
6589
|
-
const delay = Math.min(RECONNECT_BASE_MS * 2 ** (attempt - 1), RECONNECT_MAX_MS);
|
|
6590
|
-
reconnectTimer = setTimeout(connect, delay);
|
|
6591
|
-
});
|
|
6592
|
-
socket.addEventListener("error", () => {});
|
|
6593
|
-
};
|
|
6594
|
-
connect();
|
|
6595
|
-
return () => {
|
|
6596
|
-
cancelled = true;
|
|
6597
|
-
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
6598
|
-
if (socket) socket.close();
|
|
6599
|
-
};
|
|
6600
|
-
}, [
|
|
6601
|
-
port,
|
|
6602
|
-
daemonReachable,
|
|
6603
|
-
resetTick,
|
|
6604
|
-
token
|
|
6605
|
-
]);
|
|
6606
|
-
return {
|
|
6607
|
-
events,
|
|
6608
|
-
status,
|
|
6609
|
-
reset: () => {
|
|
6610
|
-
setEvents([]);
|
|
6611
|
-
setResetTick((value) => value + 1);
|
|
6612
|
-
}
|
|
6613
|
-
};
|
|
6614
|
-
};
|
|
6615
|
-
//#endregion
|
|
6616
|
-
//#region lib/tui/use-snapshot.ts
|
|
6617
|
-
const POLL_INTERVAL_MS = 3e3;
|
|
6618
|
-
const sessionSchema = z.object({
|
|
6619
|
-
channel: z.string(),
|
|
6620
|
-
connectors: z.array(z.string())
|
|
6621
|
-
});
|
|
6622
|
-
const statusResponseSchema = z.object({ clients: z.array(sessionSchema) });
|
|
6623
|
-
const emptySnapshot = {
|
|
6624
|
-
connectors: [],
|
|
6625
|
-
channels: [],
|
|
6626
|
-
profiles: [],
|
|
6627
|
-
gateway: {
|
|
6628
|
-
running: false,
|
|
6629
|
-
pid: null,
|
|
6630
|
-
port: 9742
|
|
6631
|
-
},
|
|
6632
|
-
listeners: [],
|
|
6633
|
-
sessions: [],
|
|
6634
|
-
daemonReachable: false,
|
|
6635
|
-
refreshedAt: 0
|
|
6636
|
-
};
|
|
6637
|
-
const fetchSessions = async (port, daemonRunning, token) => {
|
|
6638
|
-
if (!daemonRunning) return [];
|
|
6639
|
-
try {
|
|
6640
|
-
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
6641
|
-
const response = await fetch(`http://localhost:${port}/status`, { headers });
|
|
6642
|
-
if (!response.ok) return [];
|
|
6643
|
-
const parsed = statusResponseSchema.safeParse(await response.json());
|
|
6644
|
-
if (!parsed.success) return [];
|
|
6645
|
-
return parsed.data.clients.filter((client) => client.channel !== "*tap*");
|
|
6646
|
-
} catch {
|
|
6647
|
-
return [];
|
|
6648
|
-
}
|
|
6649
|
-
};
|
|
6650
|
-
/**
|
|
6651
|
-
* Polls Funnel state every few seconds. The returned `refresh` callback forces
|
|
6652
|
-
* an immediate refetch — used by the manual `r` key and by listener-action
|
|
6653
|
-
* keystrokes that change daemon state.
|
|
6654
|
-
*/
|
|
6655
|
-
const useSnapshot = (funnel) => {
|
|
6656
|
-
const [snapshot, setSnapshot] = useState(emptySnapshot);
|
|
6657
|
-
const [tick, setTick] = useState(0);
|
|
6658
|
-
useEffect(() => {
|
|
6659
|
-
let cancelled = false;
|
|
6660
|
-
const load = async () => {
|
|
6661
|
-
const gateway = funnel.gateway.getStatus();
|
|
6662
|
-
const token = funnel.gatewayToken.read();
|
|
6663
|
-
const [listenersResult, sessions] = await Promise.all([funnel.listeners.list(), fetchSessions(gateway.port, gateway.running, token)]);
|
|
6664
|
-
if (cancelled) return;
|
|
6665
|
-
setSnapshot({
|
|
6666
|
-
connectors: funnel.channels.listAllConnectors(),
|
|
6667
|
-
channels: funnel.channels.list(),
|
|
6668
|
-
profiles: funnel.profiles.list(),
|
|
6669
|
-
gateway,
|
|
6670
|
-
listeners: listenersResult.state === "ok" ? listenersResult.listeners : [],
|
|
6671
|
-
sessions,
|
|
6672
|
-
daemonReachable: listenersResult.state === "ok",
|
|
6673
|
-
refreshedAt: Date.now()
|
|
6674
|
-
});
|
|
6675
|
-
};
|
|
6676
|
-
load();
|
|
6677
|
-
const timer = setInterval(() => void load(), POLL_INTERVAL_MS);
|
|
6678
|
-
return () => {
|
|
6679
|
-
cancelled = true;
|
|
6680
|
-
clearInterval(timer);
|
|
6681
|
-
};
|
|
6682
|
-
}, [tick, funnel]);
|
|
6683
|
-
return {
|
|
6684
|
-
snapshot,
|
|
6685
|
-
refresh: () => setTick((value) => value + 1)
|
|
6686
|
-
};
|
|
6687
|
-
};
|
|
6688
|
-
//#endregion
|
|
6689
|
-
//#region lib/tui/components/add-row.tsx
|
|
6690
|
-
/** @jsxImportSource @opentui/react */
|
|
6691
|
-
/**
|
|
6692
|
-
* "+ add …" row used at the foot of an entity list.
|
|
6693
|
-
*
|
|
6694
|
-
* Bound to hascii's primary `HasciiButton` so AddRows match the gateway's
|
|
6695
|
-
* start / stop affordance — same neutral-white CTA chip with darken-
|
|
6696
|
-
* on-hover feel.
|
|
6697
|
-
*/
|
|
6698
|
-
function AddRow(props) {
|
|
6699
|
-
return /* @__PURE__ */ jsx(HasciiButton, {
|
|
6700
|
-
onPress: props.onClick,
|
|
6701
|
-
children: `+ ${props.label}`
|
|
6702
|
-
});
|
|
6703
|
-
}
|
|
6704
|
-
//#endregion
|
|
6705
|
-
//#region lib/tui/components/ui/hascii/card.tsx
|
|
6706
|
-
/** Filled container that frames its children. Use with HasciiCardHeader, HasciiCardContent, and HasciiCardFooter. */
|
|
6707
|
-
function HasciiCard(props) {
|
|
6708
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6709
|
-
backgroundColor: useHasciiTheme().color.card,
|
|
6710
|
-
flexDirection: "column",
|
|
6711
|
-
paddingTop: 1,
|
|
6712
|
-
paddingBottom: 1,
|
|
6713
|
-
paddingLeft: 2,
|
|
6714
|
-
paddingRight: 2,
|
|
6715
|
-
gap: 0,
|
|
6716
|
-
width: props.width,
|
|
6717
|
-
children: props.children
|
|
6718
|
-
});
|
|
6719
|
-
}
|
|
6720
|
-
//#endregion
|
|
6721
|
-
//#region lib/tui/components/ui/hascii/card-footer.tsx
|
|
6722
|
-
/** Horizontal footer region for action rows inside a HasciiCard. */
|
|
6723
|
-
function HasciiCardFooter(props) {
|
|
6724
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6725
|
-
flexDirection: "row",
|
|
6726
|
-
justifyContent: "flex-end",
|
|
6727
|
-
gap: 1,
|
|
6728
|
-
children: props.children
|
|
6729
|
-
});
|
|
6730
|
-
}
|
|
6731
|
-
//#endregion
|
|
6732
|
-
//#region lib/tui/components/ui/hascii/card-header.tsx
|
|
6733
|
-
/** Vertical heading group typically holding HasciiCardTitle and HasciiCardDescription. */
|
|
6734
|
-
function HasciiCardHeader(props) {
|
|
6735
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6736
|
-
flexDirection: "column",
|
|
6737
|
-
gap: 0,
|
|
6738
|
-
children: props.children
|
|
6739
|
-
});
|
|
6740
|
-
}
|
|
6741
|
-
//#endregion
|
|
6742
|
-
//#region lib/tui/components/ui/hascii/card-title.tsx
|
|
6743
|
-
/** Primary title text inside a card header. */
|
|
6744
|
-
function HasciiCardTitle(props) {
|
|
6745
|
-
return /* @__PURE__ */ jsx("text", {
|
|
6746
|
-
fg: useHasciiTheme().color.foreground,
|
|
6747
|
-
children: props.children
|
|
6748
|
-
});
|
|
6749
|
-
}
|
|
6750
|
-
//#endregion
|
|
6751
|
-
//#region lib/tui/components/selection-accent.tsx
|
|
6752
|
-
/** @jsxImportSource @opentui/react */
|
|
6753
|
-
const RULE_LENGTH = 20;
|
|
6754
|
-
/**
|
|
6755
|
-
* A thin `▏` (U+258F) primary-colour rule pinned to the left edge of
|
|
6756
|
-
* the parent box. Stacked tall enough to span any reasonable card or
|
|
6757
|
-
* field-group height; rows past the parent's bottom edge are clipped.
|
|
6758
|
-
*
|
|
6759
|
-
* Use inside a `position: relative` parent to mark it as the "current"
|
|
6760
|
-
* selection — same look as the sidebar `MenuItem` active accent.
|
|
6761
|
-
*/
|
|
6762
|
-
function SelectionAccent() {
|
|
6763
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6764
|
-
style: {
|
|
6765
|
-
position: "absolute",
|
|
6766
|
-
left: 0,
|
|
6767
|
-
top: 0,
|
|
6768
|
-
bottom: 0,
|
|
6769
|
-
flexDirection: "column"
|
|
6770
|
-
},
|
|
6771
|
-
children: Array.from({ length: RULE_LENGTH }, (_, index) => /* @__PURE__ */ jsx("text", {
|
|
6772
|
-
fg: funnel.primary,
|
|
6773
|
-
children: "▏"
|
|
6774
|
-
}, index))
|
|
6775
|
-
});
|
|
6776
|
-
}
|
|
6777
|
-
//#endregion
|
|
6778
|
-
//#region lib/tui/components/card.tsx
|
|
6779
|
-
/**
|
|
6780
|
-
* Per-entity form wrapper used inside the connectors / channels /
|
|
6781
|
-
* profiles / listeners views.
|
|
6782
|
-
*
|
|
6783
|
-
* Composed from hascii's `HasciiCard` family — header + body + footer.
|
|
6784
|
-
* `selected` overlays a `SelectionAccent` (▏ primary rule) at the left
|
|
6785
|
-
* edge for cursor-driven views.
|
|
6786
|
-
*/
|
|
6787
|
-
function Card(props) {
|
|
6788
|
-
return /* @__PURE__ */ jsxs(HasciiCard, { children: [
|
|
6789
|
-
props.selected ? /* @__PURE__ */ jsx(SelectionAccent, {}) : null,
|
|
6790
|
-
/* @__PURE__ */ jsx(HasciiCardHeader, { children: /* @__PURE__ */ jsx(HasciiCardTitle, { children: props.title }) }),
|
|
6791
|
-
props.children,
|
|
6792
|
-
props.onDelete ? /* @__PURE__ */ jsx(HasciiCardFooter, { children: /* @__PURE__ */ jsx(HasciiButton, {
|
|
6793
|
-
variant: "destructive",
|
|
6794
|
-
size: "sm",
|
|
6795
|
-
onPress: props.onDelete,
|
|
6796
|
-
children: "delete"
|
|
6797
|
-
}) }) : null
|
|
6798
|
-
] });
|
|
6799
|
-
}
|
|
6800
|
-
//#endregion
|
|
6801
|
-
//#region lib/tui/utils/hascii/form-item-context.tsx
|
|
6802
|
-
const FormItemContext = createContext(null);
|
|
6803
|
-
/** Read the surrounding HasciiFormItem context. Returns null when called outside one. */
|
|
6804
|
-
function useHasciiFormItem() {
|
|
6805
|
-
return useContext(FormItemContext);
|
|
6806
|
-
}
|
|
6807
|
-
/** Provider used by HasciiFormItem to share the row's focus id with the wrapped HasciiInput. */
|
|
6808
|
-
function HasciiFormItemProvider(props) {
|
|
6809
|
-
return /* @__PURE__ */ jsx(FormItemContext.Provider, {
|
|
6810
|
-
value: props.value,
|
|
6811
|
-
children: props.children
|
|
6812
|
-
});
|
|
6813
|
-
}
|
|
6814
|
-
//#endregion
|
|
6815
|
-
//#region lib/tui/utils/hascii/input-focus-context.tsx
|
|
6816
|
-
/** @jsxImportSource @opentui/react */
|
|
6817
|
-
const InputFocusContext = createContext(null);
|
|
6818
|
-
/** Returns the surrounding HasciiInputFocusProvider's API. Null when no provider is mounted. */
|
|
6819
|
-
function useHasciiInputFocus() {
|
|
6820
|
-
return useContext(InputFocusContext);
|
|
6821
|
-
}
|
|
6822
|
-
//#endregion
|
|
6823
|
-
//#region lib/tui/components/ui/hascii/form-item.tsx
|
|
6824
|
-
/** Horizontal form row: a fixed-width label on the left, the field on the right. The label background brightens while the wrapped HasciiInput is focused (requires HasciiInputFocusProvider in the tree). */
|
|
6825
|
-
function HasciiFormItem(props) {
|
|
6826
|
-
const labelWidth = props.labelWidth ?? 12;
|
|
6827
|
-
const theme = useHasciiTheme();
|
|
6828
|
-
const id = useId();
|
|
6829
|
-
const isInputFocused = useHasciiInputFocus()?.focusedId === id;
|
|
6830
|
-
const labelBg = isInputFocused ? theme.color.secondaryActive : theme.color.popover;
|
|
6831
|
-
const labelFg = isInputFocused ? theme.color.foreground : theme.color.mutedForeground;
|
|
6832
|
-
return /* @__PURE__ */ jsx(HasciiFormItemProvider, {
|
|
6833
|
-
value: { focusId: id },
|
|
6834
|
-
children: /* @__PURE__ */ jsxs("box", {
|
|
6835
|
-
flexDirection: "row",
|
|
6836
|
-
alignItems: "center",
|
|
6837
|
-
children: [/* @__PURE__ */ jsx("box", {
|
|
6838
|
-
width: labelWidth,
|
|
6839
|
-
height: 3,
|
|
6840
|
-
paddingLeft: 2,
|
|
6841
|
-
paddingRight: 2,
|
|
6842
|
-
alignItems: "flex-start",
|
|
6843
|
-
justifyContent: "center",
|
|
6844
|
-
backgroundColor: labelBg,
|
|
6845
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6846
|
-
fg: labelFg,
|
|
6847
|
-
children: props.label
|
|
6848
|
-
})
|
|
6849
|
-
}), props.children]
|
|
6850
|
-
})
|
|
6851
|
-
});
|
|
6852
|
-
}
|
|
6853
|
-
//#endregion
|
|
6854
|
-
//#region lib/tui/components/ui/hascii/input.tsx
|
|
6855
|
-
/** Single-line text input. Click to focus, Esc / outside click to blur (requires HasciiInputFocusProvider for outside click). */
|
|
6856
|
-
function HasciiInput(props) {
|
|
6857
|
-
const variant = props.variant ?? "default";
|
|
6858
|
-
const width = props.width ?? 32;
|
|
6859
|
-
const placeholder = props.placeholder ?? "";
|
|
6860
|
-
const fallbackId = useId();
|
|
6861
|
-
const id = useHasciiFormItem()?.focusId ?? fallbackId;
|
|
6862
|
-
const focusCtx = useHasciiInputFocus();
|
|
6863
|
-
const fallbackState = useState(props.defaultFocused ?? false);
|
|
6864
|
-
const isFocused = focusCtx ? focusCtx.focusedId === id : fallbackState[0];
|
|
6865
|
-
const focus = () => {
|
|
6866
|
-
if (focusCtx) focusCtx.setFocusedId(id);
|
|
6867
|
-
else fallbackState[1](true);
|
|
6868
|
-
};
|
|
6869
|
-
const blur = () => {
|
|
6870
|
-
if (focusCtx) focusCtx.setFocusedId(null);
|
|
6871
|
-
else fallbackState[1](false);
|
|
6872
|
-
};
|
|
6873
|
-
const theme = useHasciiTheme();
|
|
6874
|
-
const press = usePressable();
|
|
6875
|
-
useKeyboard((key) => {
|
|
6876
|
-
if (!isFocused) return;
|
|
6877
|
-
if (key.name === "escape") blur();
|
|
6878
|
-
});
|
|
6879
|
-
if (variant === "outline") return /* @__PURE__ */ jsx("box", {
|
|
6880
|
-
border: true,
|
|
6881
|
-
borderStyle: "rounded",
|
|
6882
|
-
borderColor: press.isPressed ? theme.color.foreground : isFocused ? theme.color.ring : press.isHovered ? theme.color.mutedForeground : theme.color.input,
|
|
6883
|
-
height: 3,
|
|
6884
|
-
width,
|
|
6885
|
-
paddingLeft: 1,
|
|
6886
|
-
paddingRight: 1,
|
|
6887
|
-
backgroundColor: theme.color.background,
|
|
6888
|
-
justifyContent: "center",
|
|
6889
|
-
...press.bind,
|
|
6890
|
-
onMouseDown: (event) => {
|
|
6891
|
-
event.stopPropagation();
|
|
6892
|
-
press.bind.onMouseDown();
|
|
6893
|
-
focus();
|
|
6894
|
-
},
|
|
6895
|
-
children: /* @__PURE__ */ jsx("input", {
|
|
6896
|
-
focused: isFocused,
|
|
6897
|
-
placeholder,
|
|
6898
|
-
value: props.value,
|
|
6899
|
-
textColor: theme.color.foreground,
|
|
6900
|
-
placeholderColor: theme.color.mutedForeground,
|
|
6901
|
-
cursorColor: theme.color.foreground,
|
|
6902
|
-
onInput: props.onInput,
|
|
6903
|
-
onChange: props.onChange
|
|
6904
|
-
})
|
|
6905
|
-
});
|
|
6906
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
6907
|
-
height: 3,
|
|
6908
|
-
width,
|
|
6909
|
-
paddingLeft: 2,
|
|
6910
|
-
paddingRight: 2,
|
|
6911
|
-
backgroundColor: press.isPressed ? theme.color.secondaryActive : isFocused || press.isHovered ? theme.color.secondaryHover : theme.color.muted,
|
|
6912
|
-
justifyContent: "center",
|
|
6913
|
-
...press.bind,
|
|
6914
|
-
onMouseDown: (event) => {
|
|
6915
|
-
event.stopPropagation();
|
|
6916
|
-
press.bind.onMouseDown();
|
|
6917
|
-
focus();
|
|
6918
|
-
},
|
|
6919
|
-
children: [/* @__PURE__ */ jsx("input", {
|
|
6920
|
-
focused: isFocused,
|
|
6921
|
-
placeholder,
|
|
6922
|
-
value: props.value,
|
|
6923
|
-
textColor: theme.color.foreground,
|
|
6924
|
-
placeholderColor: theme.color.mutedForeground,
|
|
6925
|
-
cursorColor: theme.color.foreground,
|
|
6926
|
-
onInput: props.onInput,
|
|
6927
|
-
onChange: props.onChange
|
|
6928
|
-
}), isFocused ? /* @__PURE__ */ jsx("box", {
|
|
6929
|
-
position: "absolute",
|
|
6930
|
-
bottom: 0,
|
|
6931
|
-
left: 0,
|
|
6932
|
-
right: 0,
|
|
6933
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6934
|
-
fg: theme.color.primary,
|
|
6935
|
-
children: "▁".repeat(width)
|
|
6936
|
-
})
|
|
6937
|
-
}) : null]
|
|
6938
|
-
});
|
|
6939
|
-
}
|
|
6940
|
-
//#endregion
|
|
6941
|
-
//#region lib/tui/components/editable-field.tsx
|
|
6942
|
-
/**
|
|
6943
|
-
* Inline label + input row built on hascii primitives. The hascii Input fires
|
|
6944
|
-
* `onChange` on every keystroke; we forward that to `onCommit` so callers
|
|
6945
|
-
* persist live (re-keying by `focused` still forces a remount on blur so the
|
|
6946
|
-
* input snaps back when the user clicks away without typing).
|
|
6947
|
-
*/
|
|
6948
|
-
function EditableField(props) {
|
|
6949
|
-
return /* @__PURE__ */ jsx("box", {
|
|
6950
|
-
style: { flexDirection: "row" },
|
|
6951
|
-
onMouseDown: props.onFocus,
|
|
6952
|
-
children: /* @__PURE__ */ jsx(HasciiFormItem, {
|
|
6953
|
-
label: props.label,
|
|
6954
|
-
labelWidth: 12,
|
|
6955
|
-
children: /* @__PURE__ */ jsx(HasciiInput, {
|
|
6956
|
-
value: props.initialValue,
|
|
6957
|
-
placeholder: props.placeholder,
|
|
6958
|
-
defaultFocused: props.focused,
|
|
6959
|
-
onChange: props.onCommit
|
|
6960
|
-
}, props.focused ? "focused" : "blurred")
|
|
6961
|
-
})
|
|
6962
|
-
});
|
|
6963
|
-
}
|
|
6964
|
-
//#endregion
|
|
6965
|
-
//#region lib/tui/components/panel-header.tsx
|
|
6966
|
-
/** @jsxImportSource @opentui/react */
|
|
6967
|
-
/** Dim section label rendered at the top of every Panel. */
|
|
6968
|
-
function PanelHeader(props) {
|
|
6969
|
-
const theme = useHasciiTheme();
|
|
6970
|
-
const text = props.count !== void 0 ? `${props.label} (${props.count})` : props.label;
|
|
6971
|
-
return /* @__PURE__ */ jsxs("text", {
|
|
6972
|
-
fg: theme.color.mutedForeground,
|
|
6973
|
-
children: [text, props.hint ? /* @__PURE__ */ jsx("span", {
|
|
6974
|
-
fg: funnel.faint,
|
|
6975
|
-
children: ` · ${props.hint}`
|
|
6976
|
-
}) : null]
|
|
6977
|
-
});
|
|
6978
|
-
}
|
|
6979
|
-
//#endregion
|
|
6980
|
-
//#region lib/tui/components/readonly-field.tsx
|
|
6981
|
-
/** Static label + value row that mirrors the EditableField layout. */
|
|
6982
|
-
function ReadonlyField(props) {
|
|
6983
|
-
const theme = useHasciiTheme();
|
|
6984
|
-
return /* @__PURE__ */ jsx(HasciiFormItem, {
|
|
6985
|
-
label: props.label,
|
|
6986
|
-
labelWidth: 12,
|
|
6987
|
-
children: /* @__PURE__ */ jsx("text", {
|
|
6988
|
-
fg: theme.color.foreground,
|
|
6989
|
-
children: props.value
|
|
6990
|
-
})
|
|
6991
|
-
});
|
|
6992
|
-
}
|
|
6993
|
-
//#endregion
|
|
6994
|
-
//#region lib/tui/scrollbar-options.ts
|
|
6995
|
-
/**
|
|
6996
|
-
* Shared OpenTUI scrollbar styling. Used by `ViewShell` and `DetailBar`
|
|
6997
|
-
* so both scroll containers paint the track with the surrounding
|
|
6998
|
-
* `surface` tone and the thumb in `mutedForeground` instead of OpenTUI's
|
|
6999
|
-
* default electric blue.
|
|
7000
|
-
*
|
|
7001
|
-
* Takes the active hascii theme since scrollbar coloring depends on
|
|
7002
|
-
* the same palette as everything else.
|
|
7003
|
-
*/
|
|
7004
|
-
const verticalScrollbarOptions = (theme) => ({ trackOptions: {
|
|
7005
|
-
backgroundColor: funnel.surface,
|
|
7006
|
-
foregroundColor: theme.color.mutedForeground
|
|
7007
|
-
} });
|
|
7008
|
-
//#endregion
|
|
7009
|
-
//#region lib/tui/components/view-shell.tsx
|
|
7010
|
-
/**
|
|
7011
|
-
* Outer wrapper every view renders into.
|
|
7012
|
-
*
|
|
7013
|
-
* Renders a vertical `<scrollbox>` so content that overflows the visible
|
|
7014
|
-
* area scrolls instead of clipping the layout. Padding follows the
|
|
7015
|
-
* uniform `funnel.paddingX/Y` rule and lives on the inner content
|
|
7016
|
-
* container (via `contentOptions`); the outer scrollbox itself is
|
|
7017
|
-
* transparent so multi-block views (`events` events list + DetailBar
|
|
7018
|
-
* sibling) still stack cleanly.
|
|
7019
|
-
*
|
|
7020
|
-
* The vertical scrollbar's track and thumb pull from the theme so the
|
|
7021
|
-
* widget reads as part of the surface palette instead of OpenTUI's
|
|
7022
|
-
* default electric blue.
|
|
7023
|
-
*/
|
|
7024
|
-
function ViewShell(props) {
|
|
7025
|
-
const theme = useHasciiTheme();
|
|
7026
|
-
return /* @__PURE__ */ jsx("scrollbox", {
|
|
7027
|
-
style: { flexGrow: 1 },
|
|
7028
|
-
contentOptions: {
|
|
7029
|
-
flexDirection: "column",
|
|
7030
|
-
paddingLeft: funnel.paddingX,
|
|
7031
|
-
paddingRight: funnel.paddingX,
|
|
7032
|
-
paddingTop: funnel.paddingY,
|
|
7033
|
-
paddingBottom: funnel.paddingY,
|
|
7034
|
-
gap: funnel.gap
|
|
7035
|
-
},
|
|
7036
|
-
verticalScrollbarOptions: verticalScrollbarOptions(theme),
|
|
7037
|
-
children: props.children
|
|
7038
|
-
});
|
|
7039
|
-
}
|
|
7040
|
-
//#endregion
|
|
7041
|
-
//#region lib/tui/unique-name.ts
|
|
7042
|
-
/**
|
|
7043
|
-
* Pick the first `${prefix}-N` (N = 1, 2, …) that doesn't appear in the
|
|
7044
|
-
* `existing` list.
|
|
7045
|
-
*
|
|
7046
|
-
* Used by the connectors / channels / profiles views to mint a unique
|
|
7047
|
-
* default name when the user clicks "+ add". Stops at 10 000 to avoid a
|
|
7048
|
-
* runaway loop if `existing` is full of placeholder names — falls back
|
|
7049
|
-
* to `${prefix}-${Date.now()}` so the caller still gets a legal name.
|
|
7050
|
-
*/
|
|
7051
|
-
function uniqueName(existing, prefix) {
|
|
7052
|
-
for (let i = 1; i < 1e4; i += 1) {
|
|
7053
|
-
const candidate = `${prefix}-${i}`;
|
|
7054
|
-
if (!existing.includes(candidate)) return candidate;
|
|
7055
|
-
}
|
|
7056
|
-
return `${prefix}-${Date.now()}`;
|
|
7057
|
-
}
|
|
7058
|
-
//#endregion
|
|
7059
|
-
//#region lib/tui/views/channels-view.tsx
|
|
7060
|
-
/** @jsxImportSource @opentui/react */
|
|
7061
|
-
const fieldKey$1 = (name, field) => `channels::${name}::${field}`;
|
|
7062
|
-
/**
|
|
7063
|
-
* Channel inspector — one Card per channel. Connectors live nested inside the
|
|
7064
|
-
* channel and are managed in the connectors view; here only the channel's
|
|
7065
|
-
* name and id (read-only) are shown along with a count of nested connectors.
|
|
7066
|
-
*/
|
|
7067
|
-
function ChannelsView(props) {
|
|
7068
|
-
const channels = props.snapshot.channels;
|
|
7069
|
-
const commit = (channel, field, raw) => {
|
|
7070
|
-
try {
|
|
7071
|
-
if (field === "name") {
|
|
7072
|
-
const next = raw.trim();
|
|
7073
|
-
if (next && next !== channel.name) props.funnel.channels.rename(channel.name, next);
|
|
7074
|
-
}
|
|
7075
|
-
} catch (error) {
|
|
7076
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7077
|
-
}
|
|
7078
|
-
props.setFocusedKey(null);
|
|
7079
|
-
props.refresh();
|
|
7080
|
-
};
|
|
7081
|
-
const removeChannel = (name) => {
|
|
7082
|
-
try {
|
|
7083
|
-
props.funnel.channels.remove(name);
|
|
7084
|
-
} catch (error) {
|
|
7085
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7086
|
-
}
|
|
7087
|
-
props.setFocusedKey(null);
|
|
7088
|
-
props.refresh();
|
|
7089
|
-
};
|
|
7090
|
-
const addChannel = () => {
|
|
7091
|
-
const name = uniqueName(channels.map((c) => c.name), "channel");
|
|
7092
|
-
try {
|
|
7093
|
-
const created = props.funnel.channels.add({ name });
|
|
7094
|
-
props.setFocusedKey(fieldKey$1(created.name, "name"));
|
|
7095
|
-
} catch (error) {
|
|
7096
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7097
|
-
}
|
|
7098
|
-
props.refresh();
|
|
7099
|
-
};
|
|
7100
|
-
return /* @__PURE__ */ jsxs(ViewShell, { children: [
|
|
7101
|
-
/* @__PURE__ */ jsx(PanelHeader, {
|
|
7102
|
-
label: "channels",
|
|
7103
|
-
count: channels.length
|
|
7104
|
-
}),
|
|
7105
|
-
channels.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "(none — use the button below to add one)" }) : channels.map((channel) => /* @__PURE__ */ jsxs(Card, {
|
|
7106
|
-
title: channel.name,
|
|
7107
|
-
onDelete: () => removeChannel(channel.name),
|
|
7108
|
-
children: [
|
|
7109
|
-
/* @__PURE__ */ jsx(EditableField, {
|
|
7110
|
-
label: "name",
|
|
7111
|
-
initialValue: channel.name,
|
|
7112
|
-
focused: props.focusedKey === fieldKey$1(channel.name, "name"),
|
|
7113
|
-
onFocus: () => props.setFocusedKey(fieldKey$1(channel.name, "name")),
|
|
7114
|
-
onCommit: (raw) => commit(channel, "name", raw)
|
|
7115
|
-
}),
|
|
7116
|
-
/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7117
|
-
label: "id",
|
|
7118
|
-
value: channel.id
|
|
7119
|
-
}),
|
|
7120
|
-
/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7121
|
-
label: "delivery",
|
|
7122
|
-
value: channel.delivery
|
|
7123
|
-
}),
|
|
7124
|
-
/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7125
|
-
label: "connectors",
|
|
7126
|
-
value: channel.connectors.length > 0 ? channel.connectors.map((c) => `${c.name}:${c.type}`).join(", ") : "(none)"
|
|
7127
|
-
})
|
|
7128
|
-
]
|
|
7129
|
-
}, channel.id)),
|
|
7130
|
-
/* @__PURE__ */ jsx(AddRow, {
|
|
7131
|
-
label: "add channel",
|
|
7132
|
-
onClick: addChannel
|
|
7133
|
-
})
|
|
7134
|
-
] });
|
|
7135
|
-
}
|
|
7136
|
-
//#endregion
|
|
7137
|
-
//#region lib/tui/views/connectors-view.tsx
|
|
7138
|
-
const tokenDisplay = (literal, envVar) => {
|
|
7139
|
-
if (literal !== void 0 && literal !== "") return literal;
|
|
7140
|
-
if (envVar !== void 0 && envVar !== "") return `env:${envVar}`;
|
|
7141
|
-
return "—";
|
|
7142
|
-
};
|
|
7143
|
-
const formatTimestamp = (iso) => {
|
|
7144
|
-
if (!iso) return "—";
|
|
7145
|
-
const d = new Date(iso);
|
|
7146
|
-
if (Number.isNaN(d.getTime())) return "—";
|
|
7147
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
7148
|
-
};
|
|
7149
|
-
/**
|
|
7150
|
-
* Channel-scoped connector inspector. Reads `funnel.channels.listAllConnectors()`
|
|
7151
|
-
* (already flattened with channelName / channelId tags) and lets the user delete
|
|
7152
|
-
* each connector or quickly add a new one to the first available channel via the
|
|
7153
|
-
* AddRow buttons. Editing values is intentionally read-only — token / pollInterval
|
|
7154
|
-
* mutation belongs to `fnl channels <ch> connectors set <conn> ...` because the
|
|
7155
|
-
* same connector name can exist in multiple channels and inline edits would have
|
|
7156
|
-
* to disambiguate.
|
|
7157
|
-
*/
|
|
7158
|
-
function ConnectorsView(props) {
|
|
7159
|
-
const connectors = props.snapshot.connectors;
|
|
7160
|
-
const targetChannel = props.snapshot.channels[0] ?? null;
|
|
7161
|
-
const logError = (error) => {
|
|
7162
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7163
|
-
};
|
|
7164
|
-
const removeConnector = (connector) => {
|
|
7165
|
-
props.funnel.listeners.stop(connector.channelName, connector.name).catch(logError);
|
|
7166
|
-
try {
|
|
7167
|
-
props.funnel.channels.removeConnector(connector.channelName, connector.name);
|
|
7168
|
-
} catch (error) {
|
|
7169
|
-
logError(error);
|
|
7170
|
-
}
|
|
7171
|
-
props.refresh();
|
|
7172
|
-
};
|
|
7173
|
-
const addConnector = (type) => {
|
|
7174
|
-
if (!targetChannel) {
|
|
7175
|
-
logError(/* @__PURE__ */ new Error("add a channel first before creating a connector"));
|
|
7176
|
-
return;
|
|
7177
|
-
}
|
|
7178
|
-
const name = uniqueName(connectors.filter((c) => c.channelId === targetChannel.id).map((c) => c.name), type);
|
|
7179
|
-
try {
|
|
7180
|
-
if (type === "slack") props.funnel.channels.addConnector(targetChannel.name, {
|
|
7181
|
-
type: "slack",
|
|
7182
|
-
name,
|
|
7183
|
-
botToken: "xoxb-PLACEHOLDER",
|
|
7184
|
-
appToken: "xapp-PLACEHOLDER"
|
|
7185
|
-
});
|
|
7186
|
-
else if (type === "gh") props.funnel.channels.addConnector(targetChannel.name, {
|
|
7187
|
-
type: "gh",
|
|
7188
|
-
name
|
|
7189
|
-
});
|
|
7190
|
-
else if (type === "discord") props.funnel.channels.addConnector(targetChannel.name, {
|
|
7191
|
-
type: "discord",
|
|
7192
|
-
name,
|
|
7193
|
-
botToken: "PLACEHOLDER-PLACEHOLDER"
|
|
7194
|
-
});
|
|
7195
|
-
else props.funnel.channels.addConnector(targetChannel.name, {
|
|
7196
|
-
type: "schedule",
|
|
7197
|
-
name
|
|
7198
|
-
});
|
|
7199
|
-
props.funnel.listeners.start(targetChannel.name, name).catch(logError);
|
|
7200
|
-
} catch (error) {
|
|
7201
|
-
logError(error);
|
|
7202
|
-
}
|
|
7203
|
-
props.refresh();
|
|
7204
|
-
};
|
|
7205
|
-
return /* @__PURE__ */ jsxs(ViewShell, { children: [
|
|
7206
|
-
/* @__PURE__ */ jsx(PanelHeader, {
|
|
7207
|
-
label: "connectors",
|
|
7208
|
-
count: connectors.length
|
|
7209
|
-
}),
|
|
7210
|
-
connectors.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "(none — add via the buttons below or `fnl channels <ch> connectors add ...`)" }) : connectors.map((connector) => /* @__PURE__ */ jsxs(Card, {
|
|
7211
|
-
title: connector.name,
|
|
7212
|
-
children: [
|
|
7213
|
-
/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7214
|
-
label: "channel",
|
|
7215
|
-
value: connector.channelName
|
|
7216
|
-
}),
|
|
7217
|
-
/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7218
|
-
label: "type",
|
|
7219
|
-
value: connector.type
|
|
7220
|
-
}),
|
|
7221
|
-
/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7222
|
-
label: "id",
|
|
7223
|
-
value: connector.id
|
|
7224
|
-
}),
|
|
7225
|
-
connector.type === "slack" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ReadonlyField, {
|
|
7226
|
-
label: "bot-token",
|
|
7227
|
-
value: tokenDisplay(connector.botToken, connector.botTokenEnv)
|
|
7228
|
-
}), /* @__PURE__ */ jsx(ReadonlyField, {
|
|
7229
|
-
label: "app-token",
|
|
7230
|
-
value: tokenDisplay(connector.appToken, connector.appTokenEnv)
|
|
7231
|
-
})] }) : null,
|
|
7232
|
-
connector.type === "gh" ? /* @__PURE__ */ jsx(ReadonlyField, {
|
|
7233
|
-
label: "poll",
|
|
7234
|
-
value: String(connector.pollInterval ?? 60)
|
|
7235
|
-
}) : null,
|
|
7236
|
-
connector.type === "discord" ? /* @__PURE__ */ jsx(ReadonlyField, {
|
|
7237
|
-
label: "bot-token",
|
|
7238
|
-
value: tokenDisplay(connector.botToken, connector.botTokenEnv)
|
|
7239
|
-
}) : null,
|
|
7240
|
-
connector.type === "schedule" ? /* @__PURE__ */ jsx(ReadonlyField, {
|
|
7241
|
-
label: "entries",
|
|
7242
|
-
value: String(connector.entries.length)
|
|
7243
|
-
}) : null,
|
|
7244
|
-
/* @__PURE__ */ jsx("text", {
|
|
7245
|
-
fg: funnel.faint,
|
|
7246
|
-
children: `created ${formatTimestamp(connector.createdAt)}`
|
|
7247
|
-
}),
|
|
7248
|
-
/* @__PURE__ */ jsxs("box", {
|
|
7249
|
-
style: {
|
|
7250
|
-
flexDirection: "row",
|
|
7251
|
-
justifyContent: "space-between"
|
|
7252
|
-
},
|
|
7253
|
-
children: [/* @__PURE__ */ jsx("text", {
|
|
7254
|
-
fg: funnel.faint,
|
|
7255
|
-
children: `updated ${formatTimestamp(connector.updatedAt)}`
|
|
7256
|
-
}), /* @__PURE__ */ jsx(HasciiButton, {
|
|
7257
|
-
variant: "destructive",
|
|
7258
|
-
size: "sm",
|
|
7259
|
-
onPress: () => removeConnector(connector),
|
|
7260
|
-
children: "delete"
|
|
7261
|
-
})]
|
|
7262
|
-
})
|
|
7263
|
-
]
|
|
7264
|
-
}, `${connector.channelId}::${connector.id}`)),
|
|
7265
|
-
targetChannel ? /* @__PURE__ */ jsx("text", {
|
|
7266
|
-
fg: funnel.faint,
|
|
7267
|
-
children: `add target channel: ${targetChannel.name}`
|
|
7268
|
-
}) : /* @__PURE__ */ jsx("text", {
|
|
7269
|
-
fg: funnel.warn,
|
|
7270
|
-
children: "add a channel first to enable the buttons below"
|
|
7271
|
-
}),
|
|
7272
|
-
/* @__PURE__ */ jsx(AddRow, {
|
|
7273
|
-
label: "add slack",
|
|
7274
|
-
onClick: () => addConnector("slack")
|
|
7275
|
-
}),
|
|
7276
|
-
/* @__PURE__ */ jsx(AddRow, {
|
|
7277
|
-
label: "add gh",
|
|
7278
|
-
onClick: () => addConnector("gh")
|
|
7279
|
-
}),
|
|
7280
|
-
/* @__PURE__ */ jsx(AddRow, {
|
|
7281
|
-
label: "add discord",
|
|
7282
|
-
onClick: () => addConnector("discord")
|
|
7283
|
-
}),
|
|
7284
|
-
/* @__PURE__ */ jsx(AddRow, {
|
|
7285
|
-
label: "add schedule",
|
|
7286
|
-
onClick: () => addConnector("schedule")
|
|
7287
|
-
})
|
|
7288
|
-
] });
|
|
7289
|
-
}
|
|
7290
|
-
//#endregion
|
|
7291
|
-
//#region lib/tui/components/detail-bar.tsx
|
|
7292
|
-
/**
|
|
7293
|
-
* Bottom inspector strip rendered at the foot of a view.
|
|
7294
|
-
*
|
|
7295
|
-
* Sits as a sibling of `ViewShell` (not inside it) so its background
|
|
7296
|
-
* stretches edge-to-edge across the main column and butts against the
|
|
7297
|
-
* sidebar with no horizontal gap. Background tier is `elevated` —
|
|
7298
|
-
* one step brighter than the sidebar — to read as a separate stratum
|
|
7299
|
-
* without needing a border.
|
|
7300
|
-
*
|
|
7301
|
-
* The strip is itself a `<scrollbox>` so long content (e.g., JSON for
|
|
7302
|
-
* the selected event) scrolls within the fixed `funnel.detailPanelHeight`
|
|
7303
|
-
* frame instead of pushing other UI off-screen.
|
|
7304
|
-
*/
|
|
7305
|
-
function DetailBar(props) {
|
|
7306
|
-
const theme = useHasciiTheme();
|
|
7307
|
-
return /* @__PURE__ */ jsx("scrollbox", {
|
|
7308
|
-
style: {
|
|
7309
|
-
height: funnel.detailPanelHeight,
|
|
7310
|
-
backgroundColor: theme.color.muted
|
|
7311
|
-
},
|
|
7312
|
-
contentOptions: {
|
|
7313
|
-
flexDirection: "column",
|
|
7314
|
-
paddingLeft: funnel.paddingX,
|
|
7315
|
-
paddingRight: funnel.paddingX,
|
|
7316
|
-
paddingTop: funnel.paddingY,
|
|
7317
|
-
paddingBottom: funnel.paddingY,
|
|
7318
|
-
gap: funnel.gap
|
|
7319
|
-
},
|
|
7320
|
-
verticalScrollbarOptions: verticalScrollbarOptions(theme),
|
|
7321
|
-
children: props.children
|
|
7322
|
-
});
|
|
7323
|
-
}
|
|
7324
|
-
//#endregion
|
|
7325
|
-
//#region lib/tui/components/keymap.tsx
|
|
7326
|
-
/** @jsxImportSource @opentui/react */
|
|
7327
|
-
/** Inline keymap row — used at the bottom of each interactive view. */
|
|
7328
|
-
function Keymap(props) {
|
|
7329
|
-
return /* @__PURE__ */ jsx("text", {
|
|
7330
|
-
fg: useHasciiTheme().color.mutedForeground,
|
|
7331
|
-
children: props.hints.map((hint, index) => /* @__PURE__ */ jsxs("span", { children: [
|
|
7332
|
-
index > 0 ? /* @__PURE__ */ jsx("span", {
|
|
7333
|
-
fg: funnel.faint,
|
|
7334
|
-
children: " · "
|
|
7335
|
-
}) : null,
|
|
7336
|
-
/* @__PURE__ */ jsxs("span", {
|
|
7337
|
-
fg: funnel.faint,
|
|
7338
|
-
children: [hint.key, " "]
|
|
7339
|
-
}),
|
|
7340
|
-
hint.label
|
|
7341
|
-
] }, hint.key))
|
|
7342
|
-
});
|
|
7343
|
-
}
|
|
7344
|
-
//#endregion
|
|
7345
|
-
//#region lib/tui/views/events-view.tsx
|
|
7346
|
-
/** @jsxImportSource @opentui/react */
|
|
7347
|
-
const streamLabel = (status) => {
|
|
7348
|
-
if (status === "open") return "live";
|
|
7349
|
-
if (status === "connecting") return "connecting…";
|
|
7350
|
-
if (status === "closed") return "reconnecting…";
|
|
7351
|
-
return "offline";
|
|
7352
|
-
};
|
|
7353
|
-
const formatTime = (ms) => {
|
|
7354
|
-
const date = new Date(ms);
|
|
7355
|
-
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
|
|
7356
|
-
};
|
|
7357
|
-
const truncate = (value, max) => {
|
|
7358
|
-
const flat = value.replace(/\s+/g, " ").trim();
|
|
7359
|
-
if (flat.length <= max) return flat;
|
|
7360
|
-
return `${flat.slice(0, max - 1)}…`;
|
|
7361
|
-
};
|
|
7362
|
-
const matches = (event, filter) => {
|
|
7363
|
-
if (!filter) return true;
|
|
7364
|
-
const needle = filter.toLowerCase();
|
|
7365
|
-
return [
|
|
7366
|
-
event.content,
|
|
7367
|
-
event.meta.connector ?? "",
|
|
7368
|
-
event.meta.event_type ?? "",
|
|
7369
|
-
event.meta.channel ?? ""
|
|
7370
|
-
].join(" ").toLowerCase().includes(needle);
|
|
7371
|
-
};
|
|
7372
|
-
const tryParseJson = (value) => {
|
|
7373
|
-
try {
|
|
7374
|
-
return JSON.parse(value);
|
|
7375
|
-
} catch {
|
|
7376
|
-
return value;
|
|
7377
|
-
}
|
|
7378
|
-
};
|
|
7379
|
-
const formatJson = (value) => {
|
|
7380
|
-
try {
|
|
7381
|
-
return JSON.stringify(value, null, 2);
|
|
7382
|
-
} catch {
|
|
7383
|
-
return String(value);
|
|
7384
|
-
}
|
|
7385
|
-
};
|
|
7386
|
-
/**
|
|
7387
|
-
* Live event stream + detail of the selected event.
|
|
7388
|
-
*
|
|
7389
|
-
* The events list lives inside `ViewShell` (padded canvas) while the
|
|
7390
|
-
* detail strip is a sibling `DetailBar` so its background spans the
|
|
7391
|
-
* full main column edge-to-edge and reads as a distinct elevated
|
|
7392
|
-
* stratum below the list.
|
|
7393
|
-
*/
|
|
7394
|
-
function EventsView(props) {
|
|
7395
|
-
const theme = useHasciiTheme();
|
|
7396
|
-
const visible = props.events.filter((event) => matches(event, props.filter));
|
|
7397
|
-
const selected = visible[props.selectedIndex] ?? null;
|
|
7398
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
7399
|
-
style: {
|
|
7400
|
-
flexDirection: "column",
|
|
7401
|
-
flexGrow: 1
|
|
7402
|
-
},
|
|
7403
|
-
children: [/* @__PURE__ */ jsxs(ViewShell, { children: [
|
|
7404
|
-
/* @__PURE__ */ jsx(PanelHeader, {
|
|
7405
|
-
label: "events",
|
|
7406
|
-
count: visible.length,
|
|
7407
|
-
hint: [
|
|
7408
|
-
streamLabel(props.streamStatus),
|
|
7409
|
-
`${props.events.length} total`,
|
|
7410
|
-
props.filter ? `/${props.filter}/` : null
|
|
7411
|
-
].filter((part) => part !== null).join(" · ")
|
|
7412
|
-
}),
|
|
7413
|
-
visible.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "(no events yet — waiting for the first one)" }) : visible.map((event, index) => {
|
|
7414
|
-
const isSelected = index === props.selectedIndex;
|
|
7415
|
-
const connector = event.meta.connector ?? "system";
|
|
7416
|
-
const eventType = event.meta.event_type ?? "?";
|
|
7417
|
-
return /* @__PURE__ */ jsxs("text", {
|
|
7418
|
-
bg: isSelected ? theme.color.muted : void 0,
|
|
7419
|
-
children: [
|
|
7420
|
-
/* @__PURE__ */ jsx("span", {
|
|
7421
|
-
fg: theme.color.mutedForeground,
|
|
7422
|
-
children: formatTime(event.receivedAt)
|
|
7423
|
-
}),
|
|
7424
|
-
/* @__PURE__ */ jsx("span", {
|
|
7425
|
-
fg: funnel.faint,
|
|
7426
|
-
children: " "
|
|
7427
|
-
}),
|
|
7428
|
-
/* @__PURE__ */ jsx("span", {
|
|
7429
|
-
fg: theme.color.mutedForeground,
|
|
7430
|
-
children: eventType.padEnd(8)
|
|
7431
|
-
}),
|
|
7432
|
-
/* @__PURE__ */ jsx("span", {
|
|
7433
|
-
fg: funnel.faint,
|
|
7434
|
-
children: " · "
|
|
7435
|
-
}),
|
|
7436
|
-
/* @__PURE__ */ jsx("span", {
|
|
7437
|
-
fg: isSelected ? theme.color.foreground : theme.color.foreground,
|
|
7438
|
-
children: connector.padEnd(14)
|
|
7439
|
-
}),
|
|
7440
|
-
/* @__PURE__ */ jsx("span", {
|
|
7441
|
-
fg: funnel.faint,
|
|
7442
|
-
children: " "
|
|
7443
|
-
}),
|
|
7444
|
-
/* @__PURE__ */ jsx("span", {
|
|
7445
|
-
fg: isSelected ? theme.color.foreground : theme.color.mutedForeground,
|
|
7446
|
-
children: truncate(event.content, 80)
|
|
7447
|
-
})
|
|
7448
|
-
]
|
|
7449
|
-
}, event.id);
|
|
7450
|
-
}),
|
|
7451
|
-
/* @__PURE__ */ jsx(Keymap, { hints: [{
|
|
7452
|
-
key: "j/k",
|
|
7453
|
-
label: "select"
|
|
7454
|
-
}, {
|
|
7455
|
-
key: "/",
|
|
7456
|
-
label: "filter"
|
|
7457
|
-
}] })
|
|
7458
|
-
] }), /* @__PURE__ */ jsxs(DetailBar, { children: [/* @__PURE__ */ jsx(PanelHeader, { label: "detail" }), !selected ? /* @__PURE__ */ jsx(EmptyState, { message: "(select an event with j/k to inspect)" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7459
|
-
/* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx("span", {
|
|
7460
|
-
fg: theme.color.mutedForeground,
|
|
7461
|
-
children: "meta: "
|
|
7462
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
7463
|
-
fg: theme.color.foreground,
|
|
7464
|
-
children: Object.entries(selected.meta).map(([key, value]) => `${key}=${value}`).join(" ")
|
|
7465
|
-
})] }),
|
|
7466
|
-
/* @__PURE__ */ jsx(HasciiSeparator, {}),
|
|
7467
|
-
/* @__PURE__ */ jsx("text", {
|
|
7468
|
-
fg: theme.color.foreground,
|
|
7469
|
-
children: formatJson(tryParseJson(selected.content))
|
|
7470
|
-
})
|
|
7471
|
-
] })] })]
|
|
7472
|
-
});
|
|
7473
|
-
}
|
|
7474
|
-
//#endregion
|
|
7475
|
-
//#region lib/tui/views/listeners-view.tsx
|
|
7476
|
-
/** @jsxImportSource @opentui/react */
|
|
7477
|
-
const eventCountBy = (events, connectorName) => {
|
|
7478
|
-
let count = 0;
|
|
7479
|
-
for (const event of events) if (event.meta.connector === connectorName) count += 1;
|
|
7480
|
-
return count;
|
|
7481
|
-
};
|
|
7482
|
-
/**
|
|
7483
|
-
* Listener registry — one Card per listener. The Card title shows the
|
|
7484
|
-
* listener's name; inside, a single status line carries the alive
|
|
7485
|
-
* dot, the connector type, and the event count. Cursor selection is
|
|
7486
|
-
* shown via the Card's `selected` accent. Listeners are runtime
|
|
7487
|
-
* entities derived from connectors, so there is no add path here —
|
|
7488
|
-
* register / remove a connector instead.
|
|
7489
|
-
*/
|
|
7490
|
-
function ListenersView(props) {
|
|
7491
|
-
const theme = useHasciiTheme();
|
|
7492
|
-
const listeners = props.snapshot.listeners;
|
|
7493
|
-
return /* @__PURE__ */ jsxs(ViewShell, { children: [
|
|
7494
|
-
/* @__PURE__ */ jsx(PanelHeader, {
|
|
7495
|
-
label: "listeners",
|
|
7496
|
-
count: listeners.length,
|
|
7497
|
-
hint: props.busy ? "working…" : void 0
|
|
7498
|
-
}),
|
|
7499
|
-
!props.snapshot.daemonReachable ? /* @__PURE__ */ jsx(EmptyState, { message: "(gateway daemon offline — press G to start it)" }) : listeners.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "(no listeners — register a connector first)" }) : listeners.map((entry, index) => {
|
|
7500
|
-
const aliveColor = entry.alive ? funnel.alive : funnel.dead;
|
|
7501
|
-
const count = eventCountBy(props.events, entry.name);
|
|
7502
|
-
return /* @__PURE__ */ jsx(Card, {
|
|
7503
|
-
title: entry.name,
|
|
7504
|
-
selected: index === props.selectedIndex,
|
|
7505
|
-
children: /* @__PURE__ */ jsxs("text", { children: [
|
|
7506
|
-
/* @__PURE__ */ jsx("span", {
|
|
7507
|
-
fg: aliveColor,
|
|
7508
|
-
children: entry.alive ? "●" : "○"
|
|
7509
|
-
}),
|
|
7510
|
-
/* @__PURE__ */ jsx("span", {
|
|
7511
|
-
fg: funnel.faint,
|
|
7512
|
-
children: " "
|
|
7513
|
-
}),
|
|
7514
|
-
/* @__PURE__ */ jsx("span", {
|
|
7515
|
-
fg: theme.color.mutedForeground,
|
|
7516
|
-
children: entry.type
|
|
7517
|
-
}),
|
|
7518
|
-
count > 0 ? /* @__PURE__ */ jsx("span", {
|
|
7519
|
-
fg: theme.color.mutedForeground,
|
|
7520
|
-
children: ` ${count}↓`
|
|
7521
|
-
}) : null
|
|
7522
|
-
] })
|
|
7523
|
-
}, entry.name);
|
|
7524
|
-
}),
|
|
7525
|
-
/* @__PURE__ */ jsx(Keymap, { hints: [
|
|
7526
|
-
{
|
|
7527
|
-
key: "j/k",
|
|
7528
|
-
label: "select"
|
|
7529
|
-
},
|
|
7530
|
-
{
|
|
7531
|
-
key: "s",
|
|
7532
|
-
label: "start"
|
|
7533
|
-
},
|
|
7534
|
-
{
|
|
7535
|
-
key: "x",
|
|
7536
|
-
label: "stop"
|
|
7537
|
-
},
|
|
7538
|
-
{
|
|
7539
|
-
key: "R",
|
|
7540
|
-
label: "restart"
|
|
7541
|
-
}
|
|
7542
|
-
] })
|
|
7543
|
-
] });
|
|
7544
|
-
}
|
|
7545
|
-
//#endregion
|
|
7546
|
-
//#region lib/tui/views/profiles-view.tsx
|
|
7547
|
-
/** @jsxImportSource @opentui/react */
|
|
7548
|
-
const fieldKey = (name, field) => `profiles::${name}::${field}`;
|
|
7549
|
-
/**
|
|
7550
|
-
* Profile list — one Card per profile. Selection (j/k cursor) shows the
|
|
7551
|
-
* `▏` primary rule via the Card's `selected` prop; pressing `c`
|
|
7552
|
-
* launches Claude Code with the selected profile.
|
|
7553
|
-
*
|
|
7554
|
-
* `+ add profile` at the foot creates a new profile pointed at the
|
|
7555
|
-
* first existing channel (or an empty string if there are none, which
|
|
7556
|
-
* the user must then edit before launching).
|
|
7557
|
-
*/
|
|
7558
|
-
function ProfilesView(props) {
|
|
7559
|
-
const profiles = props.snapshot.profiles;
|
|
7560
|
-
const channels = props.snapshot.channels;
|
|
7561
|
-
const commit = (profile, field, raw) => {
|
|
7562
|
-
try {
|
|
7563
|
-
if (field === "name") {
|
|
7564
|
-
const next = raw.trim();
|
|
7565
|
-
if (next && next !== profile.name) props.funnel.profiles.rename(profile.name, next);
|
|
7566
|
-
} else if (field === "channel") {
|
|
7567
|
-
const next = raw.trim();
|
|
7568
|
-
if (next) props.funnel.profiles.update(profile.name, { channelId: next });
|
|
7569
|
-
} else if (field === "path") {
|
|
7570
|
-
const next = raw.trim();
|
|
7571
|
-
if (next) props.funnel.profiles.update(profile.name, { path: next });
|
|
7572
|
-
}
|
|
7573
|
-
} catch (error) {
|
|
7574
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7575
|
-
}
|
|
7576
|
-
props.setFocusedKey(null);
|
|
7577
|
-
props.refresh();
|
|
7578
|
-
};
|
|
7579
|
-
const removeProfile = (name) => {
|
|
7580
|
-
try {
|
|
7581
|
-
props.funnel.profiles.remove(name);
|
|
7582
|
-
} catch (error) {
|
|
7583
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7584
|
-
}
|
|
7585
|
-
props.setFocusedKey(null);
|
|
7586
|
-
props.refresh();
|
|
7587
|
-
};
|
|
7588
|
-
const addProfile = () => {
|
|
7589
|
-
const name = uniqueName(profiles.map((p) => p.name), "profile");
|
|
7590
|
-
const channelId = channels[0]?.name ?? "";
|
|
7591
|
-
try {
|
|
7592
|
-
props.funnel.profiles.add({
|
|
7593
|
-
name,
|
|
7594
|
-
path: "",
|
|
7595
|
-
channelId
|
|
7596
|
-
});
|
|
7597
|
-
props.setFocusedKey(fieldKey(name, "name"));
|
|
7598
|
-
} catch (error) {
|
|
7599
|
-
props.funnel.logger?.error(error instanceof Error ? error.message : String(error));
|
|
7600
|
-
}
|
|
7601
|
-
props.refresh();
|
|
7602
|
-
};
|
|
7603
|
-
return /* @__PURE__ */ jsxs(ViewShell, { children: [
|
|
7604
|
-
/* @__PURE__ */ jsx(PanelHeader, {
|
|
7605
|
-
label: "profiles",
|
|
7606
|
-
count: profiles.length
|
|
7607
|
-
}),
|
|
7608
|
-
profiles.length === 0 ? /* @__PURE__ */ jsx(EmptyState, { message: "(none — use the button below to add one)" }) : profiles.map((profile, index) => /* @__PURE__ */ jsxs(Card, {
|
|
7609
|
-
title: profile.name,
|
|
7610
|
-
selected: index === props.selectedIndex,
|
|
7611
|
-
onDelete: () => removeProfile(profile.name),
|
|
7612
|
-
children: [
|
|
7613
|
-
/* @__PURE__ */ jsx(EditableField, {
|
|
7614
|
-
label: "name",
|
|
7615
|
-
initialValue: profile.name,
|
|
7616
|
-
focused: props.focusedKey === fieldKey(profile.name, "name"),
|
|
7617
|
-
onFocus: () => props.setFocusedKey(fieldKey(profile.name, "name")),
|
|
7618
|
-
onCommit: (raw) => commit(profile, "name", raw)
|
|
7619
|
-
}),
|
|
7620
|
-
/* @__PURE__ */ jsx(EditableField, {
|
|
7621
|
-
label: "path",
|
|
7622
|
-
initialValue: profile.path,
|
|
7623
|
-
focused: props.focusedKey === fieldKey(profile.name, "path"),
|
|
7624
|
-
onFocus: () => props.setFocusedKey(fieldKey(profile.name, "path")),
|
|
7625
|
-
onCommit: (raw) => commit(profile, "path", raw),
|
|
7626
|
-
placeholder: "repository path"
|
|
7627
|
-
}),
|
|
7628
|
-
/* @__PURE__ */ jsx(EditableField, {
|
|
7629
|
-
label: "channel",
|
|
7630
|
-
initialValue: profile.channelId,
|
|
7631
|
-
focused: props.focusedKey === fieldKey(profile.name, "channel"),
|
|
7632
|
-
onFocus: () => props.setFocusedKey(fieldKey(profile.name, "channel")),
|
|
7633
|
-
onCommit: (raw) => commit(profile, "channel", raw)
|
|
7634
|
-
})
|
|
7635
|
-
]
|
|
7636
|
-
}, profile.name)),
|
|
7637
|
-
/* @__PURE__ */ jsx(AddRow, {
|
|
7638
|
-
label: "add profile",
|
|
7639
|
-
onClick: addProfile
|
|
7640
|
-
}),
|
|
7641
|
-
/* @__PURE__ */ jsx(Keymap, { hints: [{
|
|
7642
|
-
key: "j/k",
|
|
7643
|
-
label: "select"
|
|
7644
|
-
}, {
|
|
7645
|
-
key: "c",
|
|
7646
|
-
label: "launch"
|
|
7647
|
-
}] })
|
|
7648
|
-
] });
|
|
7649
|
-
}
|
|
7650
|
-
//#endregion
|
|
7651
|
-
//#region lib/tui/app.tsx
|
|
7652
|
-
/** @jsxImportSource @opentui/react */
|
|
7653
|
-
const VIEW_KEYS = [
|
|
7654
|
-
"events",
|
|
7655
|
-
"connectors",
|
|
7656
|
-
"channels",
|
|
7657
|
-
"profiles",
|
|
7658
|
-
"listeners"
|
|
7659
|
-
];
|
|
7660
|
-
const clamp = (value, length) => {
|
|
7661
|
-
if (length === 0) return 0;
|
|
7662
|
-
if (value < 0) return 0;
|
|
7663
|
-
if (value >= length) return length - 1;
|
|
7664
|
-
return value;
|
|
7665
|
-
};
|
|
7666
|
-
const matchesEventFilter = (event, filter) => {
|
|
7667
|
-
if (!filter) return true;
|
|
7668
|
-
const needle = filter.toLowerCase();
|
|
7669
|
-
return [
|
|
7670
|
-
event.content,
|
|
7671
|
-
event.meta.connector ?? "",
|
|
7672
|
-
event.meta.event_type ?? "",
|
|
7673
|
-
event.meta.channel ?? ""
|
|
7674
|
-
].join(" ").toLowerCase().includes(needle);
|
|
7675
|
-
};
|
|
7676
|
-
/**
|
|
7677
|
-
* Funnel TUI: side-rail navigation + main content panel.
|
|
7678
|
-
*
|
|
7679
|
-
* Default landing view is the live `events` log. The sidebar shows the
|
|
7680
|
-
* gateway state, currently connected sessions, and a navigation menu
|
|
7681
|
-
* where each row carries the live count of its underlying entity.
|
|
7682
|
-
*/
|
|
7683
|
-
function App(props) {
|
|
7684
|
-
const theme = useHasciiTheme();
|
|
7685
|
-
const renderer = useRenderer();
|
|
7686
|
-
const { snapshot, refresh } = useSnapshot(props.funnel);
|
|
7687
|
-
const { events, status: streamStatus } = useEventStream(snapshot.gateway.port, snapshot.daemonReachable, props.funnel.gatewayToken.read());
|
|
7688
|
-
const [view, setViewState] = useState("events");
|
|
7689
|
-
const [mode, setMode] = useState("browse");
|
|
7690
|
-
const [listenerCursor, setListenerCursor] = useState(0);
|
|
7691
|
-
const [profileCursor, setProfileCursor] = useState(0);
|
|
7692
|
-
const [eventCursor, setEventCursor] = useState(0);
|
|
7693
|
-
const [filter, setFilter] = useState("");
|
|
7694
|
-
const [filterDraft, setFilterDraft] = useState("");
|
|
7695
|
-
const [busy, setBusy] = useState(false);
|
|
7696
|
-
const [editFocus, setEditFocus] = useState(null);
|
|
7697
|
-
const isEditing = editFocus !== null;
|
|
7698
|
-
const setView = (next) => {
|
|
7699
|
-
setViewState(next);
|
|
7700
|
-
setEditFocus(null);
|
|
7701
|
-
};
|
|
7702
|
-
const visibleEventCount = events.filter((event) => matchesEventFilter(event, filter)).length;
|
|
7703
|
-
const menuItems = [
|
|
7704
|
-
{
|
|
7705
|
-
view: "events",
|
|
7706
|
-
label: "events",
|
|
7707
|
-
count: events.length
|
|
7708
|
-
},
|
|
7709
|
-
{
|
|
7710
|
-
view: "connectors",
|
|
7711
|
-
label: "connectors",
|
|
7712
|
-
count: snapshot.connectors.length
|
|
7713
|
-
},
|
|
7714
|
-
{
|
|
7715
|
-
view: "channels",
|
|
7716
|
-
label: "channels",
|
|
7717
|
-
count: snapshot.channels.length
|
|
7718
|
-
},
|
|
7719
|
-
{
|
|
7720
|
-
view: "profiles",
|
|
7721
|
-
label: "profiles",
|
|
7722
|
-
count: snapshot.profiles.length
|
|
7723
|
-
},
|
|
7724
|
-
{
|
|
7725
|
-
view: "listeners",
|
|
7726
|
-
label: "listeners",
|
|
7727
|
-
count: snapshot.listeners.length
|
|
7728
|
-
}
|
|
7729
|
-
];
|
|
7730
|
-
const moveCursor = (delta) => {
|
|
7731
|
-
if (view === "listeners") setListenerCursor((prev) => clamp(prev + delta, snapshot.listeners.length));
|
|
7732
|
-
else if (view === "profiles") setProfileCursor((prev) => clamp(prev + delta, snapshot.profiles.length));
|
|
7733
|
-
else if (view === "events") setEventCursor((prev) => clamp(prev + delta, visibleEventCount));
|
|
7734
|
-
};
|
|
7735
|
-
const runListenerAction = async (action) => {
|
|
7736
|
-
const entry = snapshot.listeners[listenerCursor];
|
|
7737
|
-
if (!entry || busy) return;
|
|
7738
|
-
setBusy(true);
|
|
7739
|
-
if (action === "start") await props.funnel.listeners.start(entry.channelName, entry.name);
|
|
7740
|
-
if (action === "stop") await props.funnel.listeners.stop(entry.channelName, entry.name);
|
|
7741
|
-
if (action === "restart") await props.funnel.listeners.restart(entry.channelName, entry.name);
|
|
7742
|
-
setBusy(false);
|
|
7743
|
-
refresh();
|
|
7744
|
-
};
|
|
7745
|
-
const launchProfile = async () => {
|
|
7746
|
-
const profile = snapshot.profiles[profileCursor];
|
|
7747
|
-
if (!profile) return;
|
|
7748
|
-
renderer.destroy();
|
|
7749
|
-
try {
|
|
7750
|
-
await props.funnel.claude.launch({
|
|
7751
|
-
channel: profile.channelId,
|
|
7752
|
-
cwd: profile.path,
|
|
7753
|
-
profileId: profile.id,
|
|
7754
|
-
options: profile.options,
|
|
7755
|
-
env: profile.env,
|
|
7756
|
-
resume: profile.resume
|
|
7757
|
-
});
|
|
7758
|
-
} catch (error) {
|
|
7759
|
-
process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
7760
|
-
process.exit(1);
|
|
7761
|
-
}
|
|
7762
|
-
process.exit(0);
|
|
7763
|
-
};
|
|
7764
|
-
const toggleGateway = async () => {
|
|
7765
|
-
if (busy) return;
|
|
7766
|
-
setBusy(true);
|
|
7767
|
-
if (snapshot.gateway.running) await props.funnel.gateway.stop();
|
|
7768
|
-
else await props.funnel.gateway.start();
|
|
7769
|
-
setBusy(false);
|
|
7770
|
-
refresh();
|
|
7771
|
-
};
|
|
7772
|
-
useKeyboard((key) => {
|
|
7773
|
-
if (isEditing) {
|
|
7774
|
-
if (key.name === "escape") setEditFocus(null);
|
|
7775
|
-
return;
|
|
7776
|
-
}
|
|
7777
|
-
if (mode === "filter") {
|
|
7778
|
-
if (key.name === "return") {
|
|
7779
|
-
setFilter(filterDraft);
|
|
7780
|
-
setEventCursor(0);
|
|
7781
|
-
setMode("browse");
|
|
7782
|
-
return;
|
|
7783
|
-
}
|
|
7784
|
-
if (key.name === "escape") {
|
|
7785
|
-
setFilterDraft(filter);
|
|
7786
|
-
setMode("browse");
|
|
7787
|
-
return;
|
|
7788
|
-
}
|
|
7789
|
-
if (key.name === "backspace") {
|
|
7790
|
-
setFilterDraft((prev) => prev.slice(0, -1));
|
|
7791
|
-
return;
|
|
7792
|
-
}
|
|
7793
|
-
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
7794
|
-
const ch = key.sequence;
|
|
7795
|
-
if (ch >= " " && ch <= "~") setFilterDraft((prev) => prev + ch);
|
|
7796
|
-
}
|
|
7797
|
-
return;
|
|
7798
|
-
}
|
|
7799
|
-
if (mode === "profile-launcher") {
|
|
7800
|
-
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
7801
|
-
setMode("browse");
|
|
7802
|
-
return;
|
|
7803
|
-
}
|
|
7804
|
-
if (key.name === "up" || key.name === "k") {
|
|
7805
|
-
setProfileCursor((prev) => clamp(prev - 1, snapshot.profiles.length));
|
|
7806
|
-
return;
|
|
7807
|
-
}
|
|
7808
|
-
if (key.name === "down" || key.name === "j") {
|
|
7809
|
-
setProfileCursor((prev) => clamp(prev + 1, snapshot.profiles.length));
|
|
7810
|
-
return;
|
|
7811
|
-
}
|
|
7812
|
-
if (key.name === "return") launchProfile();
|
|
7813
|
-
return;
|
|
7814
|
-
}
|
|
7815
|
-
if (key.name === "q" || key.name === "escape" || key.ctrl && key.name === "c") {
|
|
7816
|
-
renderer.destroy();
|
|
7817
|
-
return;
|
|
7818
|
-
}
|
|
7819
|
-
const numericIndex = Number.parseInt(key.name ?? "", 10);
|
|
7820
|
-
const target = Number.isFinite(numericIndex) ? VIEW_KEYS[numericIndex - 1] : void 0;
|
|
7821
|
-
if (target) {
|
|
7822
|
-
setView(target);
|
|
7823
|
-
return;
|
|
7824
|
-
}
|
|
7825
|
-
if (key.name === "j" || key.name === "down") {
|
|
7826
|
-
moveCursor(1);
|
|
7827
|
-
return;
|
|
7828
|
-
}
|
|
7829
|
-
if (key.name === "k" || key.name === "up") {
|
|
7830
|
-
moveCursor(-1);
|
|
7831
|
-
return;
|
|
7832
|
-
}
|
|
7833
|
-
if (key.name === "r") {
|
|
7834
|
-
refresh();
|
|
7835
|
-
return;
|
|
7836
|
-
}
|
|
7837
|
-
if (key.name === "/" && view === "events") {
|
|
7838
|
-
setFilterDraft(filter);
|
|
7839
|
-
setMode("filter");
|
|
7840
|
-
return;
|
|
7841
|
-
}
|
|
7842
|
-
if (key.name === "c") {
|
|
7843
|
-
if (snapshot.profiles.length === 0) return;
|
|
7844
|
-
setProfileCursor((prev) => Math.min(prev, Math.max(0, snapshot.profiles.length - 1)));
|
|
7845
|
-
setMode("profile-launcher");
|
|
7846
|
-
return;
|
|
7847
|
-
}
|
|
7848
|
-
if (view === "listeners") {
|
|
7849
|
-
if (key.name === "s") {
|
|
7850
|
-
runListenerAction("start");
|
|
7851
|
-
return;
|
|
7852
|
-
}
|
|
7853
|
-
if (key.name === "x") {
|
|
7854
|
-
runListenerAction("stop");
|
|
7855
|
-
return;
|
|
7856
|
-
}
|
|
7857
|
-
if (key.name === "R" || key.shift && key.name === "r") {
|
|
7858
|
-
runListenerAction("restart");
|
|
7859
|
-
return;
|
|
7860
|
-
}
|
|
7861
|
-
}
|
|
7862
|
-
if (key.name === "G" || key.shift && key.name === "g") toggleGateway();
|
|
7863
|
-
});
|
|
7864
|
-
return /* @__PURE__ */ jsxs("box", {
|
|
7865
|
-
style: {
|
|
7866
|
-
width: "100%",
|
|
7867
|
-
height: "100%",
|
|
7868
|
-
backgroundColor: theme.color.background,
|
|
7869
|
-
flexDirection: "column"
|
|
7870
|
-
},
|
|
7871
|
-
children: [
|
|
7872
|
-
/* @__PURE__ */ jsxs("box", {
|
|
7873
|
-
style: {
|
|
7874
|
-
flexDirection: "row",
|
|
7875
|
-
flexGrow: 1
|
|
7876
|
-
},
|
|
7877
|
-
children: [/* @__PURE__ */ jsx(Sidebar, {
|
|
7878
|
-
snapshot,
|
|
7879
|
-
menuItems,
|
|
7880
|
-
view,
|
|
7881
|
-
onSelect: setView,
|
|
7882
|
-
busy,
|
|
7883
|
-
onToggleGateway: () => void toggleGateway()
|
|
7884
|
-
}), /* @__PURE__ */ jsx("box", {
|
|
7885
|
-
style: {
|
|
7886
|
-
flexDirection: "column",
|
|
7887
|
-
flexGrow: 1,
|
|
7888
|
-
backgroundColor: theme.color.background
|
|
7889
|
-
},
|
|
7890
|
-
children: view === "events" ? /* @__PURE__ */ jsx(EventsView, {
|
|
7891
|
-
events,
|
|
7892
|
-
filter,
|
|
7893
|
-
selectedIndex: eventCursor,
|
|
7894
|
-
streamStatus
|
|
7895
|
-
}) : view === "connectors" ? /* @__PURE__ */ jsx(ConnectorsView, {
|
|
7896
|
-
snapshot,
|
|
7897
|
-
funnel: props.funnel,
|
|
7898
|
-
refresh,
|
|
7899
|
-
focusedKey: editFocus,
|
|
7900
|
-
setFocusedKey: setEditFocus
|
|
7901
|
-
}) : view === "channels" ? /* @__PURE__ */ jsx(ChannelsView, {
|
|
7902
|
-
snapshot,
|
|
7903
|
-
funnel: props.funnel,
|
|
7904
|
-
refresh,
|
|
7905
|
-
focusedKey: editFocus,
|
|
7906
|
-
setFocusedKey: setEditFocus
|
|
7907
|
-
}) : view === "profiles" ? /* @__PURE__ */ jsx(ProfilesView, {
|
|
7908
|
-
snapshot,
|
|
7909
|
-
selectedIndex: profileCursor,
|
|
7910
|
-
funnel: props.funnel,
|
|
7911
|
-
refresh,
|
|
7912
|
-
focusedKey: editFocus,
|
|
7913
|
-
setFocusedKey: setEditFocus
|
|
7914
|
-
}) : /* @__PURE__ */ jsx(ListenersView, {
|
|
7915
|
-
snapshot,
|
|
7916
|
-
events,
|
|
7917
|
-
selectedIndex: listenerCursor,
|
|
7918
|
-
busy
|
|
7919
|
-
})
|
|
7920
|
-
})]
|
|
7921
|
-
}),
|
|
7922
|
-
/* @__PURE__ */ jsx(FilterInput, {
|
|
7923
|
-
value: filterDraft,
|
|
7924
|
-
active: mode === "filter"
|
|
7925
|
-
}),
|
|
7926
|
-
/* @__PURE__ */ jsx(ProfileLauncher, {
|
|
7927
|
-
active: mode === "profile-launcher",
|
|
7928
|
-
profiles: snapshot.profiles,
|
|
7929
|
-
selectedIndex: profileCursor
|
|
7930
|
-
})
|
|
7931
|
-
]
|
|
7932
|
-
});
|
|
7933
|
-
}
|
|
7934
|
-
//#endregion
|
|
7935
|
-
//#region lib/tui/tui.tsx
|
|
7936
|
-
/** @jsxImportSource @opentui/react */
|
|
7937
|
-
async function launchTui(funnel) {
|
|
7938
|
-
const renderer = await createCliRenderer();
|
|
7939
|
-
createRoot(renderer).render(/* @__PURE__ */ jsx(HasciiThemeProvider, { children: /* @__PURE__ */ jsx(App, { funnel }) }));
|
|
7940
|
-
await new Promise((resolve) => {
|
|
7941
|
-
renderer.once("destroy", () => resolve());
|
|
7942
|
-
});
|
|
7943
|
-
}
|
|
7944
|
-
//#endregion
|
|
7945
|
-
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
|
5748
|
+
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|