@xopcai/xopc 0.0.26 → 0.0.28
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/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/adapters/onboard-cli.d.ts +7 -0
- package/dist/extensions/weixin/src/adapters/onboard-cli.js +61 -0
- package/dist/extensions/weixin/src/adapters/onboard-cli.js.map +1 -0
- package/dist/extensions/weixin/src/cli/qr-login.d.ts +5 -0
- package/dist/extensions/weixin/src/cli/qr-login.js +1 -1
- package/dist/extensions/weixin/src/cli/qr-login.js.map +1 -1
- package/dist/extensions/weixin/src/index.js +1 -1
- package/dist/extensions/weixin/src/plugin.d.ts +1 -0
- package/dist/extensions/weixin/src/plugin.js +2 -0
- package/dist/extensions/weixin/src/plugin.js.map +1 -1
- package/dist/gateway/static/root/assets/{agents-Clv9i1Kb.js → agents-DplaQYS2.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-Clv9i1Kb.js.map → agents-DplaQYS2.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-DqclV-PP.js → apps-page-Co95hLOJ.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-DqclV-PP.js.map → apps-page-Co95hLOJ.js.map} +1 -1
- package/dist/gateway/static/root/assets/{channels-settings-CLyTYjrz.js → channels-settings-CkfSST0k.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-CLyTYjrz.js.map → channels-settings-CkfSST0k.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-CU8lutMt.js → cron-page-D9q6KqL8.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-CU8lutMt.js.map → cron-page-D9q6KqL8.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-utils-_UjiWax6.js → cron-utils-BmzF4m1y.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-utils-_UjiWax6.js.map → cron-utils-BmzF4m1y.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-Xqb4IGWC.js → dist-Dn-ufXyc.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-Xqb4IGWC.js.map → dist-Dn-ufXyc.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CtTUkAmw.js → extension-debug-page-BZ8xQ74_.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-CtTUkAmw.js.map → extension-debug-page-BZ8xQ74_.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-C-aQU8qR.js → extension-page-BlNgKxwW.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-C-aQU8qR.js.map → extension-page-BlNgKxwW.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-b0y9aY-q.js → extension-settings-page-CWTdW_oY.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-b0y9aY-q.js.map → extension-settings-page-CWTdW_oY.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-OT4cGzon.css +1 -0
- package/dist/gateway/static/root/assets/{index-Gr2HWo-G.js → index-lV8FGWlt.js} +94 -94
- package/dist/gateway/static/root/assets/{index-Gr2HWo-G.js.map → index-lV8FGWlt.js.map} +1 -1
- package/dist/gateway/static/root/assets/logs-page-DG31RpvG.js +2 -0
- package/dist/gateway/static/root/assets/logs-page-DG31RpvG.js.map +1 -0
- package/dist/gateway/static/root/assets/sessions-page-CdmjxDEM.js +2 -0
- package/dist/gateway/static/root/assets/{sessions-page-Cryg-36Z.js.map → sessions-page-CdmjxDEM.js.map} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-DFNKT9yg.js → settings-page-DU2XLf5s.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-page-DFNKT9yg.js.map → settings-page-DU2XLf5s.js.map} +1 -1
- package/dist/gateway/static/root/assets/{skills-page-D4gfh0Ih.js → skills-page-lb7vYtlP.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-D4gfh0Ih.js.map → skills-page-lb7vYtlP.js.map} +1 -1
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/channels/index.js +2 -2
- package/dist/src/channels/manager.js +2 -2
- package/dist/src/channels/weixin/index.js +1 -1
- package/dist/src/cli/agent-chat-log-level-preset.d.ts +7 -0
- package/dist/src/cli/agent-chat-log-level-preset.js +22 -0
- package/dist/src/cli/agent-chat-log-level-preset.js.map +1 -0
- package/dist/src/cli/commands/agent/interactive.js +4 -2
- package/dist/src/cli/commands/agent/interactive.js.map +1 -1
- package/dist/src/cli/commands/agent/stream-renderer.d.ts +14 -0
- package/dist/src/cli/commands/agent/stream-renderer.js +99 -0
- package/dist/src/cli/commands/agent/stream-renderer.js.map +1 -0
- package/dist/src/cli/commands/agent.js +2 -2
- package/dist/src/cli/commands/agent.js.map +1 -1
- package/dist/src/cli/commands/onboard.js +77 -93
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/tui.d.ts +1 -0
- package/dist/src/cli/commands/tui.js +40 -0
- package/dist/src/cli/commands/tui.js.map +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +3 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/config/schema.d.ts +6 -0
- package/dist/src/config/schema.js +6 -1
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/gateway/auth.d.ts +17 -3
- package/dist/src/gateway/auth.js +35 -16
- package/dist/src/gateway/auth.js.map +1 -1
- package/dist/src/gateway/hono/app.js +30 -1
- package/dist/src/gateway/hono/app.js.map +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +1 -1
- package/dist/src/gateway/hono/middleware/auth.js +4 -3
- package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
- package/dist/src/gateway/hono/middleware/scopes.d.ts +15 -0
- package/dist/src/gateway/hono/middleware/scopes.js +41 -0
- package/dist/src/gateway/hono/middleware/scopes.js.map +1 -0
- package/dist/src/gateway/security/audit.d.ts +18 -0
- package/dist/src/gateway/security/audit.js +68 -0
- package/dist/src/gateway/security/audit.js.map +1 -0
- package/dist/src/gateway/security/csp.d.ts +19 -0
- package/dist/src/gateway/security/csp.js +52 -0
- package/dist/src/gateway/security/csp.js.map +1 -0
- package/dist/src/gateway/security/dangerous-tools.d.ts +20 -0
- package/dist/src/gateway/security/dangerous-tools.js +46 -0
- package/dist/src/gateway/security/dangerous-tools.js.map +1 -0
- package/dist/src/gateway/security/flood-guard.d.ts +28 -0
- package/dist/src/gateway/security/flood-guard.js +42 -0
- package/dist/src/gateway/security/flood-guard.js.map +1 -0
- package/dist/src/gateway/security/index.d.ts +9 -0
- package/dist/src/gateway/security/index.js +10 -0
- package/dist/src/gateway/security/known-weak-secrets.d.ts +10 -0
- package/dist/src/gateway/security/known-weak-secrets.js +36 -0
- package/dist/src/gateway/security/known-weak-secrets.js.map +1 -0
- package/dist/src/gateway/security/operator-scopes.d.ts +37 -0
- package/dist/src/gateway/security/operator-scopes.js +137 -0
- package/dist/src/gateway/security/operator-scopes.js.map +1 -0
- package/dist/src/gateway/security/origin-check.d.ts +21 -0
- package/dist/src/gateway/security/origin-check.js +56 -0
- package/dist/src/gateway/security/origin-check.js.map +1 -0
- package/dist/src/gateway/security/preauth-connection-budget.d.ts +17 -0
- package/dist/src/gateway/security/preauth-connection-budget.js +49 -0
- package/dist/src/gateway/security/preauth-connection-budget.js.map +1 -0
- package/dist/src/gateway/security/secret-equal.d.ts +8 -0
- package/dist/src/gateway/security/secret-equal.js +30 -0
- package/dist/src/gateway/security/secret-equal.js.map +1 -0
- package/dist/src/gateway/service.d.ts +1 -1
- package/dist/src/gateway/service.js +11 -2
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/tui/backends/embedded-backend.d.ts +42 -0
- package/dist/src/tui/backends/embedded-backend.js +160 -0
- package/dist/src/tui/backends/embedded-backend.js.map +1 -0
- package/dist/src/tui/backends/gateway-sse-backend.d.ts +49 -0
- package/dist/src/tui/backends/gateway-sse-backend.js +226 -0
- package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -0
- package/dist/src/tui/components/assistant-message.d.ts +6 -0
- package/dist/src/tui/components/assistant-message.js +19 -0
- package/dist/src/tui/components/assistant-message.js.map +1 -0
- package/dist/src/tui/components/chat-log.d.ts +19 -0
- package/dist/src/tui/components/chat-log.js +99 -0
- package/dist/src/tui/components/chat-log.js.map +1 -0
- package/dist/src/tui/components/custom-editor.d.ts +13 -0
- package/dist/src/tui/components/custom-editor.js +44 -0
- package/dist/src/tui/components/custom-editor.js.map +1 -0
- package/dist/src/tui/components/tool-execution.d.ts +16 -0
- package/dist/src/tui/components/tool-execution.js +76 -0
- package/dist/src/tui/components/tool-execution.js.map +1 -0
- package/dist/src/tui/components/user-message.d.ts +6 -0
- package/dist/src/tui/components/user-message.js +22 -0
- package/dist/src/tui/components/user-message.js.map +1 -0
- package/dist/src/tui/sse-consumer.d.ts +15 -0
- package/dist/src/tui/sse-consumer.js +75 -0
- package/dist/src/tui/sse-consumer.js.map +1 -0
- package/dist/src/tui/stream-assembler.d.ts +22 -0
- package/dist/src/tui/stream-assembler.js +63 -0
- package/dist/src/tui/stream-assembler.js.map +1 -0
- package/dist/src/tui/theme.d.ts +71 -0
- package/dist/src/tui/theme.js +151 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/tui-backend.d.ts +84 -0
- package/dist/src/tui/tui-backend.js +1 -0
- package/dist/src/tui/tui-types.d.ts +85 -0
- package/dist/src/tui/tui-types.js +21 -0
- package/dist/src/tui/tui-types.js.map +1 -0
- package/dist/src/tui/tui.d.ts +3 -0
- package/dist/src/tui/tui.js +526 -0
- package/dist/src/tui/tui.js.map +1 -0
- package/package.json +9 -3
- package/dist/gateway/static/root/assets/index-DhSFfSNN.css +0 -1
- package/dist/gateway/static/root/assets/logs-page-DRI33XK4.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-DRI33XK4.js.map +0 -1
- package/dist/gateway/static/root/assets/sessions-page-Cryg-36Z.js +0 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ChatSendOptions, HistoryMessage, TuiBackend, TuiEvent, TuiModelChoice, TuiSessionItem } from '../tui-backend.js';
|
|
2
|
+
import type { SessionInfo } from '../tui-types.js';
|
|
3
|
+
interface GatewaySSEOptions {
|
|
4
|
+
url: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* TUI backend that communicates with a running xopc gateway via HTTP + SSE.
|
|
9
|
+
*
|
|
10
|
+
* - Agent streaming: `POST /api/agent` with `Accept: text/event-stream`
|
|
11
|
+
* - Broadcast events: `GET /api/events` via long-lived SSE
|
|
12
|
+
* - REST calls for sessions, models, etc.
|
|
13
|
+
*/
|
|
14
|
+
export declare class GatewaySseBackend implements TuiBackend {
|
|
15
|
+
private readonly baseUrl;
|
|
16
|
+
private readonly token;
|
|
17
|
+
private eventAbort;
|
|
18
|
+
private chatAbort;
|
|
19
|
+
onEvent?: (evt: TuiEvent) => void;
|
|
20
|
+
onConnected?: () => void;
|
|
21
|
+
onDisconnected?: (reason: string) => void;
|
|
22
|
+
constructor(opts: GatewaySSEOptions);
|
|
23
|
+
get connectionLabel(): string;
|
|
24
|
+
start(): void;
|
|
25
|
+
stop(): void;
|
|
26
|
+
sendChat(opts: ChatSendOptions): Promise<{
|
|
27
|
+
runId: string;
|
|
28
|
+
}>;
|
|
29
|
+
abortChat(opts: {
|
|
30
|
+
sessionKey: string;
|
|
31
|
+
runId: string;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
ok: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
loadHistory(opts: {
|
|
36
|
+
sessionKey: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
messages: HistoryMessage[];
|
|
40
|
+
}>;
|
|
41
|
+
listSessions(): Promise<TuiSessionItem[]>;
|
|
42
|
+
getSessionInfo(sessionKey: string): Promise<SessionInfo>;
|
|
43
|
+
listModels(): Promise<TuiModelChoice[]>;
|
|
44
|
+
resetSession(sessionKey: string): Promise<void>;
|
|
45
|
+
patchSession(sessionKey: string, patch: Record<string, unknown>): Promise<void>;
|
|
46
|
+
private startEventStream;
|
|
47
|
+
private scheduleReconnect;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createLogger } from "../../utils/logger/index.js";
|
|
2
|
+
import { init_logger } from "../../utils/logger.js";
|
|
3
|
+
import { prependEnvelopeTimestamp } from "../../channels/envelope-timestamp.js";
|
|
4
|
+
import { consumeSSEStream, parseSSEData } from "../sse-consumer.js";
|
|
5
|
+
//#region src/tui/backends/gateway-sse-backend.ts
|
|
6
|
+
init_logger();
|
|
7
|
+
const log = createLogger("TUI:GatewaySSE");
|
|
8
|
+
/** Fetch wrapper that adds auth headers. */
|
|
9
|
+
async function gatewayFetch(baseUrl, path, token, init) {
|
|
10
|
+
const headers = {
|
|
11
|
+
"Content-Type": "application/json",
|
|
12
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
13
|
+
...init?.headers
|
|
14
|
+
};
|
|
15
|
+
return fetch(`${baseUrl}${path}`, {
|
|
16
|
+
...init,
|
|
17
|
+
headers
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* TUI backend that communicates with a running xopc gateway via HTTP + SSE.
|
|
22
|
+
*
|
|
23
|
+
* - Agent streaming: `POST /api/agent` with `Accept: text/event-stream`
|
|
24
|
+
* - Broadcast events: `GET /api/events` via long-lived SSE
|
|
25
|
+
* - REST calls for sessions, models, etc.
|
|
26
|
+
*/
|
|
27
|
+
var GatewaySseBackend = class {
|
|
28
|
+
baseUrl;
|
|
29
|
+
token;
|
|
30
|
+
eventAbort = null;
|
|
31
|
+
chatAbort = null;
|
|
32
|
+
onEvent;
|
|
33
|
+
onConnected;
|
|
34
|
+
onDisconnected;
|
|
35
|
+
constructor(opts) {
|
|
36
|
+
this.baseUrl = opts.url.replace(/\/+$/, "");
|
|
37
|
+
this.token = opts.token;
|
|
38
|
+
}
|
|
39
|
+
get connectionLabel() {
|
|
40
|
+
return this.baseUrl;
|
|
41
|
+
}
|
|
42
|
+
start() {
|
|
43
|
+
this.startEventStream();
|
|
44
|
+
}
|
|
45
|
+
stop() {
|
|
46
|
+
this.eventAbort?.abort();
|
|
47
|
+
this.eventAbort = null;
|
|
48
|
+
this.chatAbort?.abort();
|
|
49
|
+
this.chatAbort = null;
|
|
50
|
+
}
|
|
51
|
+
async sendChat(opts) {
|
|
52
|
+
this.chatAbort?.abort();
|
|
53
|
+
this.chatAbort = new AbortController();
|
|
54
|
+
const signal = this.chatAbort.signal;
|
|
55
|
+
const runId = crypto.randomUUID();
|
|
56
|
+
(async () => {
|
|
57
|
+
try {
|
|
58
|
+
const res = await gatewayFetch(this.baseUrl, "/api/agent", this.token, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { Accept: "text/event-stream" },
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
message: opts.message.trimStart().startsWith("/") ? opts.message : prependEnvelopeTimestamp(opts.message),
|
|
63
|
+
channel: "webchat",
|
|
64
|
+
sessionKey: opts.sessionKey,
|
|
65
|
+
thinking: opts.thinking
|
|
66
|
+
}),
|
|
67
|
+
signal
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const body = await res.json().catch(() => ({}));
|
|
71
|
+
this.onEvent?.({
|
|
72
|
+
event: "error",
|
|
73
|
+
data: { content: body.error?.message ?? `Gateway error: ${res.status}` }
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if ((res.headers.get("Content-Type") ?? "").includes("text/event-stream") && res.body) await consumeSSEStream(res.body, (sseEvent) => {
|
|
78
|
+
if (signal.aborted) return;
|
|
79
|
+
const data = parseSSEData(sseEvent.data);
|
|
80
|
+
if (!data) return;
|
|
81
|
+
this.onEvent?.({
|
|
82
|
+
event: sseEvent.event,
|
|
83
|
+
data
|
|
84
|
+
});
|
|
85
|
+
}, signal);
|
|
86
|
+
else {
|
|
87
|
+
const json = await res.json();
|
|
88
|
+
if (json.ok && json.payload?.content) {
|
|
89
|
+
this.onEvent?.({
|
|
90
|
+
event: "token",
|
|
91
|
+
data: { content: json.payload.content }
|
|
92
|
+
});
|
|
93
|
+
this.onEvent?.({
|
|
94
|
+
event: "result",
|
|
95
|
+
data: { ok: true }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (signal.aborted) return;
|
|
101
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
102
|
+
this.onEvent?.({
|
|
103
|
+
event: "error",
|
|
104
|
+
data: { content: errorMessage }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
return { runId };
|
|
109
|
+
}
|
|
110
|
+
async abortChat(opts) {
|
|
111
|
+
this.chatAbort?.abort();
|
|
112
|
+
this.chatAbort = null;
|
|
113
|
+
try {
|
|
114
|
+
return { ok: (await (await gatewayFetch(this.baseUrl, "/api/agent/abort", this.token, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ runId: opts.runId })
|
|
117
|
+
})).json()).ok ?? false };
|
|
118
|
+
} catch {
|
|
119
|
+
return { ok: false };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async loadHistory(opts) {
|
|
123
|
+
try {
|
|
124
|
+
const params = new URLSearchParams({ key: opts.sessionKey });
|
|
125
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
126
|
+
const res = await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(opts.sessionKey)}/messages?${params}`, this.token);
|
|
127
|
+
if (!res.ok) return { messages: [] };
|
|
128
|
+
return { messages: (await res.json()).payload?.messages ?? [] };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
131
|
+
log.warn({
|
|
132
|
+
err: error,
|
|
133
|
+
errorMessage
|
|
134
|
+
}, `Failed to load history: ${errorMessage}`);
|
|
135
|
+
return { messages: [] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async listSessions() {
|
|
139
|
+
try {
|
|
140
|
+
const res = await gatewayFetch(this.baseUrl, "/api/sessions", this.token);
|
|
141
|
+
if (!res.ok) return [];
|
|
142
|
+
return (await res.json()).payload?.sessions ?? [];
|
|
143
|
+
} catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async getSessionInfo(sessionKey) {
|
|
148
|
+
try {
|
|
149
|
+
const res = await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(sessionKey)}`, this.token);
|
|
150
|
+
if (!res.ok) return {};
|
|
151
|
+
return (await res.json()).payload ?? {};
|
|
152
|
+
} catch {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async listModels() {
|
|
157
|
+
try {
|
|
158
|
+
const res = await gatewayFetch(this.baseUrl, "/api/models", this.token);
|
|
159
|
+
if (!res.ok) return [];
|
|
160
|
+
return (await res.json()).payload?.models ?? [];
|
|
161
|
+
} catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async resetSession(sessionKey) {
|
|
166
|
+
await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(sessionKey)}`, this.token, { method: "DELETE" }).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
async patchSession(sessionKey, patch) {
|
|
169
|
+
await gatewayFetch(this.baseUrl, `/api/sessions/${encodeURIComponent(sessionKey)}`, this.token, {
|
|
170
|
+
method: "PATCH",
|
|
171
|
+
body: JSON.stringify(patch)
|
|
172
|
+
}).catch(() => {});
|
|
173
|
+
}
|
|
174
|
+
startEventStream() {
|
|
175
|
+
this.eventAbort?.abort();
|
|
176
|
+
this.eventAbort = new AbortController();
|
|
177
|
+
const url = new URL(`${this.baseUrl}/api/events`);
|
|
178
|
+
if (this.token) url.searchParams.set("token", this.token);
|
|
179
|
+
const connect = async () => {
|
|
180
|
+
try {
|
|
181
|
+
const res = await fetch(url.toString(), {
|
|
182
|
+
signal: this.eventAbort.signal,
|
|
183
|
+
headers: { Accept: "text/event-stream" }
|
|
184
|
+
});
|
|
185
|
+
if (!res.ok || !res.body) {
|
|
186
|
+
this.onDisconnected?.(`event stream error: ${res.status}`);
|
|
187
|
+
this.scheduleReconnect();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.onConnected?.();
|
|
191
|
+
await consumeSSEStream(res.body, (sseEvent) => {
|
|
192
|
+
if (sseEvent.event === "connected") return;
|
|
193
|
+
const data = parseSSEData(sseEvent.data);
|
|
194
|
+
if (data !== null) this.onEvent?.({
|
|
195
|
+
event: sseEvent.event,
|
|
196
|
+
data
|
|
197
|
+
});
|
|
198
|
+
}, this.eventAbort.signal);
|
|
199
|
+
if (!this.eventAbort?.signal.aborted) {
|
|
200
|
+
this.onDisconnected?.("stream closed");
|
|
201
|
+
this.scheduleReconnect();
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (this.eventAbort?.signal.aborted) return;
|
|
205
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
206
|
+
log.warn({
|
|
207
|
+
err: error,
|
|
208
|
+
errorMessage
|
|
209
|
+
}, `Event stream failed: ${errorMessage}`);
|
|
210
|
+
this.onDisconnected?.(errorMessage);
|
|
211
|
+
this.scheduleReconnect();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
connect();
|
|
215
|
+
}
|
|
216
|
+
scheduleReconnect() {
|
|
217
|
+
if (this.eventAbort?.signal.aborted) return;
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
if (!this.eventAbort?.signal.aborted) this.startEventStream();
|
|
220
|
+
}, 3e3);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
//#endregion
|
|
224
|
+
export { GatewaySseBackend };
|
|
225
|
+
|
|
226
|
+
//# sourceMappingURL=gateway-sse-backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-sse-backend.js","names":[],"sources":["../../../../src/tui/backends/gateway-sse-backend.ts"],"sourcesContent":["import { prependEnvelopeTimestamp } from '../../channels/envelope-timestamp.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { consumeSSEStream, parseSSEData } from '../sse-consumer.js';\nimport type {\n ChatSendOptions,\n HistoryMessage,\n TuiBackend,\n TuiEvent,\n TuiModelChoice,\n TuiSessionItem,\n} from '../tui-backend.js';\nimport type { SessionInfo } from '../tui-types.js';\n\nconst log = createLogger('TUI:GatewaySSE');\n\ninterface GatewaySSEOptions {\n url: string;\n token?: string;\n}\n\n/** Fetch wrapper that adds auth headers. */\nasync function gatewayFetch(\n baseUrl: string,\n path: string,\n token: string | undefined,\n init?: RequestInit,\n): Promise<Response> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...(init?.headers as Record<string, string> | undefined),\n };\n return fetch(`${baseUrl}${path}`, { ...init, headers });\n}\n\n/**\n * TUI backend that communicates with a running xopc gateway via HTTP + SSE.\n *\n * - Agent streaming: `POST /api/agent` with `Accept: text/event-stream`\n * - Broadcast events: `GET /api/events` via long-lived SSE\n * - REST calls for sessions, models, etc.\n */\nexport class GatewaySseBackend implements TuiBackend {\n private readonly baseUrl: string;\n private readonly token: string | undefined;\n private eventAbort: AbortController | null = null;\n private chatAbort: AbortController | null = null;\n\n onEvent?: (evt: TuiEvent) => void;\n onConnected?: () => void;\n onDisconnected?: (reason: string) => void;\n\n constructor(opts: GatewaySSEOptions) {\n this.baseUrl = opts.url.replace(/\\/+$/, '');\n this.token = opts.token;\n }\n\n get connectionLabel(): string {\n return this.baseUrl;\n }\n\n start(): void {\n this.startEventStream();\n }\n\n stop(): void {\n this.eventAbort?.abort();\n this.eventAbort = null;\n this.chatAbort?.abort();\n this.chatAbort = null;\n }\n\n // ── Agent chat (POST /api/agent → SSE response body) ──\n\n async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {\n this.chatAbort?.abort();\n this.chatAbort = new AbortController();\n const signal = this.chatAbort.signal;\n const runId = crypto.randomUUID();\n\n // Fire-and-forget: run the HTTP request + SSE consumption in background\n // so the TUI event loop stays responsive for keyboard input.\n void (async () => {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/agent', this.token, {\n method: 'POST',\n headers: { Accept: 'text/event-stream' },\n body: JSON.stringify({\n // Prepend envelope timestamp for regular messages so the model knows\n // the current date/time. Skip for slash commands — parseSlashCommand\n // requires lines starting with '/'.\n message: opts.message.trimStart().startsWith('/')\n ? opts.message\n : prependEnvelopeTimestamp(opts.message),\n channel: 'webchat',\n sessionKey: opts.sessionKey,\n thinking: opts.thinking,\n }),\n signal,\n });\n\n if (!res.ok) {\n const body = (await res.json().catch(() => ({}))) as { error?: { message?: string } };\n this.onEvent?.({\n event: 'error',\n data: { content: body.error?.message ?? `Gateway error: ${res.status}` },\n });\n return;\n }\n\n const contentType = res.headers.get('Content-Type') ?? '';\n\n if (contentType.includes('text/event-stream') && res.body) {\n await consumeSSEStream(\n res.body,\n (sseEvent) => {\n if (signal.aborted) return;\n const data = parseSSEData<Record<string, unknown>>(sseEvent.data);\n if (!data) return;\n this.onEvent?.({ event: sseEvent.event, data });\n },\n signal,\n );\n } else {\n const json = (await res.json()) as { ok?: boolean; payload?: { content?: string } };\n if (json.ok && json.payload?.content) {\n this.onEvent?.({\n event: 'token',\n data: { content: json.payload.content },\n });\n this.onEvent?.({ event: 'result', data: { ok: true } });\n }\n }\n } catch (error) {\n if (signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n this.onEvent?.({ event: 'error', data: { content: errorMessage } });\n }\n })();\n\n return { runId };\n }\n\n async abortChat(opts: { sessionKey: string; runId: string }): Promise<{ ok: boolean }> {\n this.chatAbort?.abort();\n this.chatAbort = null;\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/agent/abort', this.token, {\n method: 'POST',\n body: JSON.stringify({ runId: opts.runId }),\n });\n const json = (await res.json()) as { ok?: boolean };\n return { ok: json.ok ?? false };\n } catch {\n return { ok: false };\n }\n }\n\n // ── REST helpers ──\n\n async loadHistory(opts: {\n sessionKey: string;\n limit?: number;\n }): Promise<{ messages: HistoryMessage[] }> {\n try {\n const params = new URLSearchParams({ key: opts.sessionKey });\n if (opts.limit) params.set('limit', String(opts.limit));\n const res = await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(opts.sessionKey)}/messages?${params}`,\n this.token,\n );\n if (!res.ok) return { messages: [] };\n const json = (await res.json()) as { ok?: boolean; payload?: { messages?: HistoryMessage[] } };\n return { messages: json.payload?.messages ?? [] };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Failed to load history: ${errorMessage}`);\n return { messages: [] };\n }\n }\n\n async listSessions(): Promise<TuiSessionItem[]> {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/sessions', this.token);\n if (!res.ok) return [];\n const json = (await res.json()) as {\n ok?: boolean;\n payload?: { sessions?: TuiSessionItem[] };\n };\n return json.payload?.sessions ?? [];\n } catch {\n return [];\n }\n }\n\n async getSessionInfo(sessionKey: string): Promise<SessionInfo> {\n try {\n const res = await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n );\n if (!res.ok) return {};\n const json = (await res.json()) as { ok?: boolean; payload?: SessionInfo };\n return json.payload ?? {};\n } catch {\n return {};\n }\n }\n\n async listModels(): Promise<TuiModelChoice[]> {\n try {\n const res = await gatewayFetch(this.baseUrl, '/api/models', this.token);\n if (!res.ok) return [];\n const json = (await res.json()) as {\n ok?: boolean;\n payload?: { models?: TuiModelChoice[] };\n };\n return json.payload?.models ?? [];\n } catch {\n return [];\n }\n }\n\n async resetSession(sessionKey: string): Promise<void> {\n await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n { method: 'DELETE' },\n ).catch(() => {});\n }\n\n async patchSession(sessionKey: string, patch: Record<string, unknown>): Promise<void> {\n await gatewayFetch(\n this.baseUrl,\n `/api/sessions/${encodeURIComponent(sessionKey)}`,\n this.token,\n { method: 'PATCH', body: JSON.stringify(patch) },\n ).catch(() => {});\n }\n\n // ── Broadcast SSE (GET /api/events) ──\n\n private startEventStream(): void {\n this.eventAbort?.abort();\n this.eventAbort = new AbortController();\n\n const url = new URL(`${this.baseUrl}/api/events`);\n if (this.token) url.searchParams.set('token', this.token);\n\n const connect = async () => {\n try {\n const res = await fetch(url.toString(), {\n signal: this.eventAbort!.signal,\n headers: { Accept: 'text/event-stream' },\n });\n\n if (!res.ok || !res.body) {\n this.onDisconnected?.(`event stream error: ${res.status}`);\n this.scheduleReconnect();\n return;\n }\n\n this.onConnected?.();\n\n await consumeSSEStream(\n res.body,\n (sseEvent) => {\n if (sseEvent.event === 'connected') return;\n const data = parseSSEData(sseEvent.data);\n if (data !== null) {\n this.onEvent?.({ event: sseEvent.event, data });\n }\n },\n this.eventAbort!.signal,\n );\n\n // Stream ended normally\n if (!this.eventAbort?.signal.aborted) {\n this.onDisconnected?.('stream closed');\n this.scheduleReconnect();\n }\n } catch (error) {\n if (this.eventAbort?.signal.aborted) return;\n const errorMessage = error instanceof Error ? error.message : String(error);\n log.warn({ err: error, errorMessage }, `Event stream failed: ${errorMessage}`);\n this.onDisconnected?.(errorMessage);\n this.scheduleReconnect();\n }\n };\n\n void connect();\n }\n\n private scheduleReconnect(): void {\n if (this.eventAbort?.signal.aborted) return;\n setTimeout(() => {\n if (!this.eventAbort?.signal.aborted) {\n this.startEventStream();\n }\n }, 3000);\n }\n}\n"],"mappings":";;;;;aACqD;AAYrD,MAAM,MAAM,aAAa,iBAAiB;;AAQ1C,eAAe,aACb,SACA,MACA,OACA,MACmB;CACnB,MAAM,UAAkC;EACtC,gBAAgB;EAChB,GAAI,QAAQ,EAAE,eAAe,UAAU,SAAS,GAAG,EAAE;EACrD,GAAI,MAAM;EACX;AACD,QAAO,MAAM,GAAG,UAAU,QAAQ;EAAE,GAAG;EAAM;EAAS,CAAC;;;;;;;;;AAUzD,IAAa,oBAAb,MAAqD;CACnD;CACA;CACA,aAA6C;CAC7C,YAA4C;CAE5C;CACA;CACA;CAEA,YAAY,MAAyB;AACnC,OAAK,UAAU,KAAK,IAAI,QAAQ,QAAQ,GAAG;AAC3C,OAAK,QAAQ,KAAK;;CAGpB,IAAI,kBAA0B;AAC5B,SAAO,KAAK;;CAGd,QAAc;AACZ,OAAK,kBAAkB;;CAGzB,OAAa;AACX,OAAK,YAAY,OAAO;AACxB,OAAK,aAAa;AAClB,OAAK,WAAW,OAAO;AACvB,OAAK,YAAY;;CAKnB,MAAM,SAAS,MAAmD;AAChE,OAAK,WAAW,OAAO;AACvB,OAAK,YAAY,IAAI,iBAAiB;EACtC,MAAM,SAAS,KAAK,UAAU;EAC9B,MAAM,QAAQ,OAAO,YAAY;AAIjC,GAAM,YAAY;AAChB,OAAI;IACF,MAAM,MAAM,MAAM,aAAa,KAAK,SAAS,cAAc,KAAK,OAAO;KACrE,QAAQ;KACR,SAAS,EAAE,QAAQ,qBAAqB;KACxC,MAAM,KAAK,UAAU;MAInB,SAAS,KAAK,QAAQ,WAAW,CAAC,WAAW,IAAI,GAC7C,KAAK,UACL,yBAAyB,KAAK,QAAQ;MAC1C,SAAS;MACT,YAAY,KAAK;MACjB,UAAU,KAAK;MAChB,CAAC;KACF;KACD,CAAC;AAEF,QAAI,CAAC,IAAI,IAAI;KACX,MAAM,OAAQ,MAAM,IAAI,MAAM,CAAC,aAAa,EAAE,EAAE;AAChD,UAAK,UAAU;MACb,OAAO;MACP,MAAM,EAAE,SAAS,KAAK,OAAO,WAAW,kBAAkB,IAAI,UAAU;MACzE,CAAC;AACF;;AAKF,SAFoB,IAAI,QAAQ,IAAI,eAAe,IAAI,IAEvC,SAAS,oBAAoB,IAAI,IAAI,KACnD,OAAM,iBACJ,IAAI,OACH,aAAa;AACZ,SAAI,OAAO,QAAS;KACpB,MAAM,OAAO,aAAsC,SAAS,KAAK;AACjE,SAAI,CAAC,KAAM;AACX,UAAK,UAAU;MAAE,OAAO,SAAS;MAAO;MAAM,CAAC;OAEjD,OACD;SACI;KACL,MAAM,OAAQ,MAAM,IAAI,MAAM;AAC9B,SAAI,KAAK,MAAM,KAAK,SAAS,SAAS;AACpC,WAAK,UAAU;OACb,OAAO;OACP,MAAM,EAAE,SAAS,KAAK,QAAQ,SAAS;OACxC,CAAC;AACF,WAAK,UAAU;OAAE,OAAO;OAAU,MAAM,EAAE,IAAI,MAAM;OAAE,CAAC;;;YAGpD,OAAO;AACd,QAAI,OAAO,QAAS;IACpB,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,SAAK,UAAU;KAAE,OAAO;KAAS,MAAM,EAAE,SAAS,cAAc;KAAE,CAAC;;MAEnE;AAEJ,SAAO,EAAE,OAAO;;CAGlB,MAAM,UAAU,MAAuE;AACrF,OAAK,WAAW,OAAO;AACvB,OAAK,YAAY;AACjB,MAAI;AAMF,UAAO,EAAE,KAAI,OADO,MAJF,aAAa,KAAK,SAAS,oBAAoB,KAAK,OAAO;IAC3E,QAAQ;IACR,MAAM,KAAK,UAAU,EAAE,OAAO,KAAK,OAAO,CAAC;IAC5C,CAAC,EACsB,MAAM,EACZ,MAAM,OAAO;UACzB;AACN,UAAO,EAAE,IAAI,OAAO;;;CAMxB,MAAM,YAAY,MAG0B;AAC1C,MAAI;GACF,MAAM,SAAS,IAAI,gBAAgB,EAAE,KAAK,KAAK,YAAY,CAAC;AAC5D,OAAI,KAAK,MAAO,QAAO,IAAI,SAAS,OAAO,KAAK,MAAM,CAAC;GACvD,MAAM,MAAM,MAAM,aAChB,KAAK,SACL,iBAAiB,mBAAmB,KAAK,WAAW,CAAC,YAAY,UACjE,KAAK,MACN;AACD,OAAI,CAAC,IAAI,GAAI,QAAO,EAAE,UAAU,EAAE,EAAE;AAEpC,UAAO,EAAE,WAAU,MADC,IAAI,MAAM,EACN,SAAS,YAAY,EAAE,EAAE;WAC1C,OAAO;GACd,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,OAAI,KAAK;IAAE,KAAK;IAAO;IAAc,EAAE,2BAA2B,eAAe;AACjF,UAAO,EAAE,UAAU,EAAE,EAAE;;;CAI3B,MAAM,eAA0C;AAC9C,MAAI;GACF,MAAM,MAAM,MAAM,aAAa,KAAK,SAAS,iBAAiB,KAAK,MAAM;AACzE,OAAI,CAAC,IAAI,GAAI,QAAO,EAAE;AAKtB,WAAO,MAJa,IAAI,MAAM,EAIlB,SAAS,YAAY,EAAE;UAC7B;AACN,UAAO,EAAE;;;CAIb,MAAM,eAAe,YAA0C;AAC7D,MAAI;GACF,MAAM,MAAM,MAAM,aAChB,KAAK,SACL,iBAAiB,mBAAmB,WAAW,IAC/C,KAAK,MACN;AACD,OAAI,CAAC,IAAI,GAAI,QAAO,EAAE;AAEtB,WAAO,MADa,IAAI,MAAM,EAClB,WAAW,EAAE;UACnB;AACN,UAAO,EAAE;;;CAIb,MAAM,aAAwC;AAC5C,MAAI;GACF,MAAM,MAAM,MAAM,aAAa,KAAK,SAAS,eAAe,KAAK,MAAM;AACvE,OAAI,CAAC,IAAI,GAAI,QAAO,EAAE;AAKtB,WAAO,MAJa,IAAI,MAAM,EAIlB,SAAS,UAAU,EAAE;UAC3B;AACN,UAAO,EAAE;;;CAIb,MAAM,aAAa,YAAmC;AACpD,QAAM,aACJ,KAAK,SACL,iBAAiB,mBAAmB,WAAW,IAC/C,KAAK,OACL,EAAE,QAAQ,UAAU,CACrB,CAAC,YAAY,GAAG;;CAGnB,MAAM,aAAa,YAAoB,OAA+C;AACpF,QAAM,aACJ,KAAK,SACL,iBAAiB,mBAAmB,WAAW,IAC/C,KAAK,OACL;GAAE,QAAQ;GAAS,MAAM,KAAK,UAAU,MAAM;GAAE,CACjD,CAAC,YAAY,GAAG;;CAKnB,mBAAiC;AAC/B,OAAK,YAAY,OAAO;AACxB,OAAK,aAAa,IAAI,iBAAiB;EAEvC,MAAM,MAAM,IAAI,IAAI,GAAG,KAAK,QAAQ,aAAa;AACjD,MAAI,KAAK,MAAO,KAAI,aAAa,IAAI,SAAS,KAAK,MAAM;EAEzD,MAAM,UAAU,YAAY;AAC1B,OAAI;IACF,MAAM,MAAM,MAAM,MAAM,IAAI,UAAU,EAAE;KACtC,QAAQ,KAAK,WAAY;KACzB,SAAS,EAAE,QAAQ,qBAAqB;KACzC,CAAC;AAEF,QAAI,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM;AACxB,UAAK,iBAAiB,uBAAuB,IAAI,SAAS;AAC1D,UAAK,mBAAmB;AACxB;;AAGF,SAAK,eAAe;AAEpB,UAAM,iBACJ,IAAI,OACH,aAAa;AACZ,SAAI,SAAS,UAAU,YAAa;KACpC,MAAM,OAAO,aAAa,SAAS,KAAK;AACxC,SAAI,SAAS,KACX,MAAK,UAAU;MAAE,OAAO,SAAS;MAAO;MAAM,CAAC;OAGnD,KAAK,WAAY,OAClB;AAGD,QAAI,CAAC,KAAK,YAAY,OAAO,SAAS;AACpC,UAAK,iBAAiB,gBAAgB;AACtC,UAAK,mBAAmB;;YAEnB,OAAO;AACd,QAAI,KAAK,YAAY,OAAO,QAAS;IACrC,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,QAAI,KAAK;KAAE,KAAK;KAAO;KAAc,EAAE,wBAAwB,eAAe;AAC9E,SAAK,iBAAiB,aAAa;AACnC,SAAK,mBAAmB;;;AAIvB,WAAS;;CAGhB,oBAAkC;AAChC,MAAI,KAAK,YAAY,OAAO,QAAS;AACrC,mBAAiB;AACf,OAAI,CAAC,KAAK,YAAY,OAAO,QAC3B,MAAK,kBAAkB;KAExB,IAAK"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { markdownTheme, theme } from "../theme.js";
|
|
2
|
+
import { Container, Markdown, Spacer } from "@mariozechner/pi-tui";
|
|
3
|
+
//#region src/tui/components/assistant-message.ts
|
|
4
|
+
var AssistantMessageComponent = class extends Container {
|
|
5
|
+
body;
|
|
6
|
+
constructor(text) {
|
|
7
|
+
super();
|
|
8
|
+
this.body = new Markdown(text, 0, 0, markdownTheme, { color: (line) => theme.assistantText(line) });
|
|
9
|
+
this.addChild(new Spacer(1));
|
|
10
|
+
this.addChild(this.body);
|
|
11
|
+
}
|
|
12
|
+
setText(text) {
|
|
13
|
+
this.body.setText(text);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { AssistantMessageComponent };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=assistant-message.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-message.js","names":[],"sources":["../../../../src/tui/components/assistant-message.ts"],"sourcesContent":["import { Container, Markdown, Spacer } from '@mariozechner/pi-tui';\n\nimport { markdownTheme, theme } from '../theme.js';\n\nexport class AssistantMessageComponent extends Container {\n private body: Markdown;\n\n constructor(text: string) {\n super();\n this.body = new Markdown(text, 0, 0, markdownTheme, {\n color: (line) => theme.assistantText(line),\n });\n this.addChild(new Spacer(1));\n this.addChild(this.body);\n }\n\n setText(text: string): void {\n this.body.setText(text);\n }\n}\n"],"mappings":";;;AAIA,IAAa,4BAAb,cAA+C,UAAU;CACvD;CAEA,YAAY,MAAc;AACxB,SAAO;AACP,OAAK,OAAO,IAAI,SAAS,MAAM,GAAG,GAAG,eAAe,EAClD,QAAQ,SAAS,MAAM,cAAc,KAAK,EAC3C,CAAC;AACF,OAAK,SAAS,IAAI,OAAO,EAAE,CAAC;AAC5B,OAAK,SAAS,KAAK,KAAK;;CAG1B,QAAQ,MAAoB;AAC1B,OAAK,KAAK,QAAQ,KAAK"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Container } from '@mariozechner/pi-tui';
|
|
2
|
+
export declare class ChatLog extends Container {
|
|
3
|
+
private toolById;
|
|
4
|
+
private streamingRuns;
|
|
5
|
+
private toolsExpanded;
|
|
6
|
+
private pruneOverflow;
|
|
7
|
+
private dropReferences;
|
|
8
|
+
private append;
|
|
9
|
+
clearAll(): void;
|
|
10
|
+
addSystem(text: string): void;
|
|
11
|
+
addUser(text: string): void;
|
|
12
|
+
startAssistant(text: string, runId: string): void;
|
|
13
|
+
updateAssistant(text: string, runId: string): void;
|
|
14
|
+
finalizeAssistant(text: string, runId: string): void;
|
|
15
|
+
dropAssistant(runId: string): void;
|
|
16
|
+
startTool(toolCallId: string, toolName: string, args: unknown): void;
|
|
17
|
+
updateToolResult(toolCallId: string, result: string, isError: boolean): void;
|
|
18
|
+
setToolsExpanded(expanded: boolean): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { theme } from "../theme.js";
|
|
2
|
+
import { AssistantMessageComponent } from "./assistant-message.js";
|
|
3
|
+
import { ToolExecutionComponent } from "./tool-execution.js";
|
|
4
|
+
import { UserMessageComponent } from "./user-message.js";
|
|
5
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
6
|
+
//#region src/tui/components/chat-log.ts
|
|
7
|
+
const MAX_COMPONENTS = 180;
|
|
8
|
+
var ChatLog = class extends Container {
|
|
9
|
+
toolById = /* @__PURE__ */ new Map();
|
|
10
|
+
streamingRuns = /* @__PURE__ */ new Map();
|
|
11
|
+
toolsExpanded = false;
|
|
12
|
+
pruneOverflow() {
|
|
13
|
+
while (this.children.length > MAX_COMPONENTS) {
|
|
14
|
+
const oldest = this.children[0];
|
|
15
|
+
if (!oldest) return;
|
|
16
|
+
this.removeChild(oldest);
|
|
17
|
+
this.dropReferences(oldest);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
dropReferences(component) {
|
|
21
|
+
for (const [id, tool] of this.toolById.entries()) if (tool === component) this.toolById.delete(id);
|
|
22
|
+
for (const [runId, msg] of this.streamingRuns.entries()) if (msg === component) this.streamingRuns.delete(runId);
|
|
23
|
+
}
|
|
24
|
+
append(component) {
|
|
25
|
+
this.addChild(component);
|
|
26
|
+
this.pruneOverflow();
|
|
27
|
+
}
|
|
28
|
+
clearAll() {
|
|
29
|
+
this.clear();
|
|
30
|
+
this.toolById.clear();
|
|
31
|
+
this.streamingRuns.clear();
|
|
32
|
+
}
|
|
33
|
+
addSystem(text) {
|
|
34
|
+
const entry = new Container();
|
|
35
|
+
entry.addChild(new Spacer(1));
|
|
36
|
+
entry.addChild(new Text(theme.system(text), 1, 0));
|
|
37
|
+
this.append(entry);
|
|
38
|
+
}
|
|
39
|
+
addUser(text) {
|
|
40
|
+
this.append(new UserMessageComponent(text));
|
|
41
|
+
}
|
|
42
|
+
startAssistant(text, runId) {
|
|
43
|
+
const existing = this.streamingRuns.get(runId);
|
|
44
|
+
if (existing) {
|
|
45
|
+
existing.setText(text);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const component = new AssistantMessageComponent(text);
|
|
49
|
+
this.streamingRuns.set(runId, component);
|
|
50
|
+
this.append(component);
|
|
51
|
+
}
|
|
52
|
+
updateAssistant(text, runId) {
|
|
53
|
+
const existing = this.streamingRuns.get(runId);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
this.startAssistant(text, runId);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
existing.setText(text);
|
|
59
|
+
}
|
|
60
|
+
finalizeAssistant(text, runId) {
|
|
61
|
+
const existing = this.streamingRuns.get(runId);
|
|
62
|
+
if (existing) {
|
|
63
|
+
existing.setText(text);
|
|
64
|
+
this.streamingRuns.delete(runId);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.append(new AssistantMessageComponent(text));
|
|
68
|
+
}
|
|
69
|
+
dropAssistant(runId) {
|
|
70
|
+
const existing = this.streamingRuns.get(runId);
|
|
71
|
+
if (!existing) return;
|
|
72
|
+
this.removeChild(existing);
|
|
73
|
+
this.streamingRuns.delete(runId);
|
|
74
|
+
}
|
|
75
|
+
startTool(toolCallId, toolName, args) {
|
|
76
|
+
const existing = this.toolById.get(toolCallId);
|
|
77
|
+
if (existing) {
|
|
78
|
+
existing.setArgs(args);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const component = new ToolExecutionComponent(toolName, args);
|
|
82
|
+
component.setExpanded(this.toolsExpanded);
|
|
83
|
+
this.toolById.set(toolCallId, component);
|
|
84
|
+
this.append(component);
|
|
85
|
+
}
|
|
86
|
+
updateToolResult(toolCallId, result, isError) {
|
|
87
|
+
const existing = this.toolById.get(toolCallId);
|
|
88
|
+
if (!existing) return;
|
|
89
|
+
existing.setResult(result, isError);
|
|
90
|
+
}
|
|
91
|
+
setToolsExpanded(expanded) {
|
|
92
|
+
this.toolsExpanded = expanded;
|
|
93
|
+
for (const tool of this.toolById.values()) tool.setExpanded(expanded);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
export { ChatLog };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=chat-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-log.js","names":[],"sources":["../../../../src/tui/components/chat-log.ts"],"sourcesContent":["import type { Component } from '@mariozechner/pi-tui';\nimport { Container, Spacer, Text } from '@mariozechner/pi-tui';\n\nimport { theme } from '../theme.js';\nimport { AssistantMessageComponent } from './assistant-message.js';\nimport { ToolExecutionComponent } from './tool-execution.js';\nimport { UserMessageComponent } from './user-message.js';\n\nconst MAX_COMPONENTS = 180;\n\nexport class ChatLog extends Container {\n private toolById = new Map<string, ToolExecutionComponent>();\n private streamingRuns = new Map<string, AssistantMessageComponent>();\n private toolsExpanded = false;\n\n private pruneOverflow(): void {\n while (this.children.length > MAX_COMPONENTS) {\n const oldest = this.children[0];\n if (!oldest) return;\n this.removeChild(oldest);\n this.dropReferences(oldest);\n }\n }\n\n private dropReferences(component: Component): void {\n for (const [id, tool] of this.toolById.entries()) {\n if (tool === component) this.toolById.delete(id);\n }\n for (const [runId, msg] of this.streamingRuns.entries()) {\n if (msg === component) this.streamingRuns.delete(runId);\n }\n }\n\n private append(component: Component): void {\n this.addChild(component);\n this.pruneOverflow();\n }\n\n clearAll(): void {\n this.clear();\n this.toolById.clear();\n this.streamingRuns.clear();\n }\n\n addSystem(text: string): void {\n const entry = new Container();\n entry.addChild(new Spacer(1));\n entry.addChild(new Text(theme.system(text), 1, 0));\n this.append(entry);\n }\n\n addUser(text: string): void {\n this.append(new UserMessageComponent(text));\n }\n\n startAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (existing) {\n existing.setText(text);\n return;\n }\n const component = new AssistantMessageComponent(text);\n this.streamingRuns.set(runId, component);\n this.append(component);\n }\n\n updateAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (!existing) {\n this.startAssistant(text, runId);\n return;\n }\n existing.setText(text);\n }\n\n finalizeAssistant(text: string, runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (existing) {\n existing.setText(text);\n this.streamingRuns.delete(runId);\n return;\n }\n this.append(new AssistantMessageComponent(text));\n }\n\n dropAssistant(runId: string): void {\n const existing = this.streamingRuns.get(runId);\n if (!existing) return;\n this.removeChild(existing);\n this.streamingRuns.delete(runId);\n }\n\n startTool(toolCallId: string, toolName: string, args: unknown): void {\n const existing = this.toolById.get(toolCallId);\n if (existing) {\n existing.setArgs(args);\n return;\n }\n const component = new ToolExecutionComponent(toolName, args);\n component.setExpanded(this.toolsExpanded);\n this.toolById.set(toolCallId, component);\n this.append(component);\n }\n\n updateToolResult(toolCallId: string, result: string, isError: boolean): void {\n const existing = this.toolById.get(toolCallId);\n if (!existing) return;\n existing.setResult(result, isError);\n }\n\n setToolsExpanded(expanded: boolean): void {\n this.toolsExpanded = expanded;\n for (const tool of this.toolById.values()) {\n tool.setExpanded(expanded);\n }\n }\n}\n"],"mappings":";;;;;;AAQA,MAAM,iBAAiB;AAEvB,IAAa,UAAb,cAA6B,UAAU;CACrC,2BAAmB,IAAI,KAAqC;CAC5D,gCAAwB,IAAI,KAAwC;CACpE,gBAAwB;CAExB,gBAA8B;AAC5B,SAAO,KAAK,SAAS,SAAS,gBAAgB;GAC5C,MAAM,SAAS,KAAK,SAAS;AAC7B,OAAI,CAAC,OAAQ;AACb,QAAK,YAAY,OAAO;AACxB,QAAK,eAAe,OAAO;;;CAI/B,eAAuB,WAA4B;AACjD,OAAK,MAAM,CAAC,IAAI,SAAS,KAAK,SAAS,SAAS,CAC9C,KAAI,SAAS,UAAW,MAAK,SAAS,OAAO,GAAG;AAElD,OAAK,MAAM,CAAC,OAAO,QAAQ,KAAK,cAAc,SAAS,CACrD,KAAI,QAAQ,UAAW,MAAK,cAAc,OAAO,MAAM;;CAI3D,OAAe,WAA4B;AACzC,OAAK,SAAS,UAAU;AACxB,OAAK,eAAe;;CAGtB,WAAiB;AACf,OAAK,OAAO;AACZ,OAAK,SAAS,OAAO;AACrB,OAAK,cAAc,OAAO;;CAG5B,UAAU,MAAoB;EAC5B,MAAM,QAAQ,IAAI,WAAW;AAC7B,QAAM,SAAS,IAAI,OAAO,EAAE,CAAC;AAC7B,QAAM,SAAS,IAAI,KAAK,MAAM,OAAO,KAAK,EAAE,GAAG,EAAE,CAAC;AAClD,OAAK,OAAO,MAAM;;CAGpB,QAAQ,MAAoB;AAC1B,OAAK,OAAO,IAAI,qBAAqB,KAAK,CAAC;;CAG7C,eAAe,MAAc,OAAqB;EAChD,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,UAAU;AACZ,YAAS,QAAQ,KAAK;AACtB;;EAEF,MAAM,YAAY,IAAI,0BAA0B,KAAK;AACrD,OAAK,cAAc,IAAI,OAAO,UAAU;AACxC,OAAK,OAAO,UAAU;;CAGxB,gBAAgB,MAAc,OAAqB;EACjD,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,CAAC,UAAU;AACb,QAAK,eAAe,MAAM,MAAM;AAChC;;AAEF,WAAS,QAAQ,KAAK;;CAGxB,kBAAkB,MAAc,OAAqB;EACnD,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,UAAU;AACZ,YAAS,QAAQ,KAAK;AACtB,QAAK,cAAc,OAAO,MAAM;AAChC;;AAEF,OAAK,OAAO,IAAI,0BAA0B,KAAK,CAAC;;CAGlD,cAAc,OAAqB;EACjC,MAAM,WAAW,KAAK,cAAc,IAAI,MAAM;AAC9C,MAAI,CAAC,SAAU;AACf,OAAK,YAAY,SAAS;AAC1B,OAAK,cAAc,OAAO,MAAM;;CAGlC,UAAU,YAAoB,UAAkB,MAAqB;EACnE,MAAM,WAAW,KAAK,SAAS,IAAI,WAAW;AAC9C,MAAI,UAAU;AACZ,YAAS,QAAQ,KAAK;AACtB;;EAEF,MAAM,YAAY,IAAI,uBAAuB,UAAU,KAAK;AAC5D,YAAU,YAAY,KAAK,cAAc;AACzC,OAAK,SAAS,IAAI,YAAY,UAAU;AACxC,OAAK,OAAO,UAAU;;CAGxB,iBAAiB,YAAoB,QAAgB,SAAwB;EAC3E,MAAM,WAAW,KAAK,SAAS,IAAI,WAAW;AAC9C,MAAI,CAAC,SAAU;AACf,WAAS,UAAU,QAAQ,QAAQ;;CAGrC,iBAAiB,UAAyB;AACxC,OAAK,gBAAgB;AACrB,OAAK,MAAM,QAAQ,KAAK,SAAS,QAAQ,CACvC,MAAK,YAAY,SAAS"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Editor } from '@mariozechner/pi-tui';
|
|
2
|
+
/**
|
|
3
|
+
* Extended editor with additional key bindings for the TUI.
|
|
4
|
+
*/
|
|
5
|
+
export declare class CustomEditor extends Editor {
|
|
6
|
+
onEscape?: () => void;
|
|
7
|
+
onCtrlC?: () => void;
|
|
8
|
+
onCtrlD?: () => void;
|
|
9
|
+
onCtrlL?: () => void;
|
|
10
|
+
onCtrlO?: () => void;
|
|
11
|
+
onCtrlT?: () => void;
|
|
12
|
+
handleInput(data: string): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Editor, Key, matchesKey } from "@mariozechner/pi-tui";
|
|
2
|
+
//#region src/tui/components/custom-editor.ts
|
|
3
|
+
/**
|
|
4
|
+
* Extended editor with additional key bindings for the TUI.
|
|
5
|
+
*/
|
|
6
|
+
var CustomEditor = class extends Editor {
|
|
7
|
+
onEscape;
|
|
8
|
+
onCtrlC;
|
|
9
|
+
onCtrlD;
|
|
10
|
+
onCtrlL;
|
|
11
|
+
onCtrlO;
|
|
12
|
+
onCtrlT;
|
|
13
|
+
handleInput(data) {
|
|
14
|
+
if (matchesKey(data, Key.ctrl("l")) && this.onCtrlL) {
|
|
15
|
+
this.onCtrlL();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (matchesKey(data, Key.ctrl("o")) && this.onCtrlO) {
|
|
19
|
+
this.onCtrlO();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (matchesKey(data, Key.ctrl("t")) && this.onCtrlT) {
|
|
23
|
+
this.onCtrlT();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (matchesKey(data, Key.escape) && this.onEscape && !this.isShowingAutocomplete()) {
|
|
27
|
+
this.onEscape();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (matchesKey(data, Key.ctrl("c")) && this.onCtrlC) {
|
|
31
|
+
this.onCtrlC();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (matchesKey(data, Key.ctrl("d"))) {
|
|
35
|
+
if (this.getText().length === 0 && this.onCtrlD) this.onCtrlD();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
super.handleInput(data);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { CustomEditor };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=custom-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-editor.js","names":[],"sources":["../../../../src/tui/components/custom-editor.ts"],"sourcesContent":["import { Editor, Key, matchesKey } from '@mariozechner/pi-tui';\n\n/**\n * Extended editor with additional key bindings for the TUI.\n */\nexport class CustomEditor extends Editor {\n onEscape?: () => void;\n onCtrlC?: () => void;\n onCtrlD?: () => void;\n onCtrlL?: () => void;\n onCtrlO?: () => void;\n onCtrlT?: () => void;\n\n handleInput(data: string): void {\n if (matchesKey(data, Key.ctrl('l')) && this.onCtrlL) {\n this.onCtrlL();\n return;\n }\n if (matchesKey(data, Key.ctrl('o')) && this.onCtrlO) {\n this.onCtrlO();\n return;\n }\n if (matchesKey(data, Key.ctrl('t')) && this.onCtrlT) {\n this.onCtrlT();\n return;\n }\n if (matchesKey(data, Key.escape) && this.onEscape && !this.isShowingAutocomplete()) {\n this.onEscape();\n return;\n }\n if (matchesKey(data, Key.ctrl('c')) && this.onCtrlC) {\n this.onCtrlC();\n return;\n }\n if (matchesKey(data, Key.ctrl('d'))) {\n if (this.getText().length === 0 && this.onCtrlD) {\n this.onCtrlD();\n }\n return;\n }\n super.handleInput(data);\n }\n}\n"],"mappings":";;;;;AAKA,IAAa,eAAb,cAAkC,OAAO;CACvC;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,MAAoB;AAC9B,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,OAAO,IAAI,KAAK,YAAY,CAAC,KAAK,uBAAuB,EAAE;AAClF,QAAK,UAAU;AACf;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,SAAS;AACnD,QAAK,SAAS;AACd;;AAEF,MAAI,WAAW,MAAM,IAAI,KAAK,IAAI,CAAC,EAAE;AACnC,OAAI,KAAK,SAAS,CAAC,WAAW,KAAK,KAAK,QACtC,MAAK,SAAS;AAEhB;;AAEF,QAAM,YAAY,KAAK"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Container } from '@mariozechner/pi-tui';
|
|
2
|
+
export declare class ToolExecutionComponent extends Container {
|
|
3
|
+
private contentText;
|
|
4
|
+
private toolName;
|
|
5
|
+
private args;
|
|
6
|
+
private resultText;
|
|
7
|
+
private expanded;
|
|
8
|
+
private isError;
|
|
9
|
+
private isPartial;
|
|
10
|
+
constructor(toolName: string, args: unknown);
|
|
11
|
+
setArgs(args: unknown): void;
|
|
12
|
+
setExpanded(expanded: boolean): void;
|
|
13
|
+
setResult(result: string, isError: boolean): void;
|
|
14
|
+
private refresh;
|
|
15
|
+
private formatToolExecution;
|
|
16
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { theme } from "../theme.js";
|
|
2
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
//#region src/tui/components/tool-execution.ts
|
|
4
|
+
const MAX_ARG_VALUE_LENGTH = 120;
|
|
5
|
+
function sanitize(text) {
|
|
6
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
7
|
+
}
|
|
8
|
+
function formatArgsSummary(args) {
|
|
9
|
+
if (!args || typeof args !== "object") return "";
|
|
10
|
+
const entries = Object.entries(args);
|
|
11
|
+
if (entries.length === 0) return "";
|
|
12
|
+
return entries.map(([key, value]) => {
|
|
13
|
+
const stringValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
14
|
+
return `${key}=${stringValue.length > MAX_ARG_VALUE_LENGTH ? `${stringValue.slice(0, MAX_ARG_VALUE_LENGTH - 3)}...` : stringValue}`;
|
|
15
|
+
}).join(", ");
|
|
16
|
+
}
|
|
17
|
+
var ToolExecutionComponent = class extends Container {
|
|
18
|
+
contentText;
|
|
19
|
+
toolName;
|
|
20
|
+
args;
|
|
21
|
+
resultText = "";
|
|
22
|
+
expanded = false;
|
|
23
|
+
isError = false;
|
|
24
|
+
isPartial = true;
|
|
25
|
+
constructor(toolName, args) {
|
|
26
|
+
super();
|
|
27
|
+
this.toolName = toolName;
|
|
28
|
+
this.args = args;
|
|
29
|
+
this.addChild(new Spacer(1));
|
|
30
|
+
const bgFn = (text) => theme.toolPendingBg(text);
|
|
31
|
+
this.contentText = new Text("", 1, 1, bgFn);
|
|
32
|
+
this.addChild(this.contentText);
|
|
33
|
+
this.refresh();
|
|
34
|
+
}
|
|
35
|
+
setArgs(args) {
|
|
36
|
+
this.args = args;
|
|
37
|
+
this.refresh();
|
|
38
|
+
}
|
|
39
|
+
setExpanded(expanded) {
|
|
40
|
+
this.expanded = expanded;
|
|
41
|
+
this.refresh();
|
|
42
|
+
}
|
|
43
|
+
setResult(result, isError) {
|
|
44
|
+
this.resultText = sanitize(result);
|
|
45
|
+
this.isError = isError;
|
|
46
|
+
this.isPartial = false;
|
|
47
|
+
this.refresh();
|
|
48
|
+
}
|
|
49
|
+
refresh() {
|
|
50
|
+
const bgFn = this.isPartial ? (text) => theme.toolPendingBg(text) : this.isError ? (text) => theme.toolErrorBg(text) : (text) => theme.toolSuccessBg(text);
|
|
51
|
+
this.contentText.setCustomBgFn(bgFn);
|
|
52
|
+
this.contentText.setText(this.formatToolExecution());
|
|
53
|
+
}
|
|
54
|
+
formatToolExecution() {
|
|
55
|
+
const argsStr = formatArgsSummary(this.args);
|
|
56
|
+
const titleParts = [theme.toolTitle(theme.bold(this.toolName))];
|
|
57
|
+
if (argsStr) titleParts.push(theme.dim(`(${argsStr})`));
|
|
58
|
+
let text = titleParts.join(" ");
|
|
59
|
+
const output = this.resultText;
|
|
60
|
+
if (output) if (this.expanded) text += `\n${theme.toolOutput(output)}`;
|
|
61
|
+
else {
|
|
62
|
+
const oneLine = output.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
|
|
63
|
+
const lineCount = output.split("\n").length;
|
|
64
|
+
const maxPreviewLength = 200;
|
|
65
|
+
const preview = oneLine.length > maxPreviewLength ? `${oneLine.slice(0, maxPreviewLength)}…` : oneLine;
|
|
66
|
+
const suffix = lineCount > 1 ? theme.dim(` (${lineCount} lines)`) : "";
|
|
67
|
+
text += `\n${theme.toolOutput(preview)}${suffix}`;
|
|
68
|
+
}
|
|
69
|
+
else if (this.isPartial) text += `\n${theme.dim("…")}`;
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { ToolExecutionComponent };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=tool-execution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-execution.js","names":[],"sources":["../../../../src/tui/components/tool-execution.ts"],"sourcesContent":["import { Container, Spacer, Text } from '@mariozechner/pi-tui';\n\nimport { theme } from '../theme.js';\n\nconst MAX_ARG_VALUE_LENGTH = 120;\n\nfunction sanitize(text: string): string {\n return text.replace(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g, '');\n}\n\nfunction formatArgsSummary(args: unknown): string {\n if (!args || typeof args !== 'object') return '';\n const entries = Object.entries(args as Record<string, unknown>);\n if (entries.length === 0) return '';\n return entries\n .map(([key, value]) => {\n const stringValue = typeof value === 'string' ? value : JSON.stringify(value);\n const truncated =\n stringValue.length > MAX_ARG_VALUE_LENGTH\n ? `${stringValue.slice(0, MAX_ARG_VALUE_LENGTH - 3)}...`\n : stringValue;\n return `${key}=${truncated}`;\n })\n .join(', ');\n}\n\nexport class ToolExecutionComponent extends Container {\n private contentText: Text;\n private toolName: string;\n private args: unknown;\n private resultText = '';\n private expanded = false;\n private isError = false;\n private isPartial = true;\n\n constructor(toolName: string, args: unknown) {\n super();\n this.toolName = toolName;\n this.args = args;\n\n this.addChild(new Spacer(1));\n\n const bgFn = (text: string) => theme.toolPendingBg(text);\n this.contentText = new Text('', 1, 1, bgFn);\n this.addChild(this.contentText);\n\n this.refresh();\n }\n\n setArgs(args: unknown): void {\n this.args = args;\n this.refresh();\n }\n\n setExpanded(expanded: boolean): void {\n this.expanded = expanded;\n this.refresh();\n }\n\n setResult(result: string, isError: boolean): void {\n this.resultText = sanitize(result);\n this.isError = isError;\n this.isPartial = false;\n this.refresh();\n }\n\n private refresh(): void {\n const bgFn = this.isPartial\n ? (text: string) => theme.toolPendingBg(text)\n : this.isError\n ? (text: string) => theme.toolErrorBg(text)\n : (text: string) => theme.toolSuccessBg(text);\n this.contentText.setCustomBgFn(bgFn);\n this.contentText.setText(this.formatToolExecution());\n }\n\n private formatToolExecution(): string {\n // Title line: 🔧 tool_name (args_summary)\n const argsStr = formatArgsSummary(this.args);\n const titleParts = [theme.toolTitle(theme.bold(this.toolName))];\n if (argsStr) {\n titleParts.push(theme.dim(`(${argsStr})`));\n }\n let text = titleParts.join(' ');\n\n // Output / result — collapsed: single-line summary; expanded: full output\n const output = this.resultText;\n if (output) {\n if (this.expanded) {\n text += `\\n${theme.toolOutput(output)}`;\n } else {\n // Compact single-line preview: flatten to one line and truncate\n const oneLine = output.replace(/\\n/g, ' ').replace(/\\s+/g, ' ').trim();\n const lineCount = output.split('\\n').length;\n const maxPreviewLength = 200;\n const preview = oneLine.length > maxPreviewLength\n ? `${oneLine.slice(0, maxPreviewLength)}…`\n : oneLine;\n const suffix = lineCount > 1 ? theme.dim(` (${lineCount} lines)`) : '';\n text += `\\n${theme.toolOutput(preview)}${suffix}`;\n }\n } else if (this.isPartial) {\n text += `\\n${theme.dim('…')}`;\n }\n\n return text;\n }\n}\n"],"mappings":";;;AAIA,MAAM,uBAAuB;AAE7B,SAAS,SAAS,MAAsB;AACtC,QAAO,KAAK,QAAQ,iCAAiC,GAAG;;AAG1D,SAAS,kBAAkB,MAAuB;AAChD,KAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;CAC9C,MAAM,UAAU,OAAO,QAAQ,KAAgC;AAC/D,KAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAO,QACJ,KAAK,CAAC,KAAK,WAAW;EACrB,MAAM,cAAc,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,MAAM;AAK7E,SAAO,GAAG,IAAI,GAHZ,YAAY,SAAS,uBACjB,GAAG,YAAY,MAAM,GAAG,uBAAuB,EAAE,CAAC,OAClD;GAEN,CACD,KAAK,KAAK;;AAGf,IAAa,yBAAb,cAA4C,UAAU;CACpD;CACA;CACA;CACA,aAAqB;CACrB,WAAmB;CACnB,UAAkB;CAClB,YAAoB;CAEpB,YAAY,UAAkB,MAAe;AAC3C,SAAO;AACP,OAAK,WAAW;AAChB,OAAK,OAAO;AAEZ,OAAK,SAAS,IAAI,OAAO,EAAE,CAAC;EAE5B,MAAM,QAAQ,SAAiB,MAAM,cAAc,KAAK;AACxD,OAAK,cAAc,IAAI,KAAK,IAAI,GAAG,GAAG,KAAK;AAC3C,OAAK,SAAS,KAAK,YAAY;AAE/B,OAAK,SAAS;;CAGhB,QAAQ,MAAqB;AAC3B,OAAK,OAAO;AACZ,OAAK,SAAS;;CAGhB,YAAY,UAAyB;AACnC,OAAK,WAAW;AAChB,OAAK,SAAS;;CAGhB,UAAU,QAAgB,SAAwB;AAChD,OAAK,aAAa,SAAS,OAAO;AAClC,OAAK,UAAU;AACf,OAAK,YAAY;AACjB,OAAK,SAAS;;CAGhB,UAAwB;EACtB,MAAM,OAAO,KAAK,aACb,SAAiB,MAAM,cAAc,KAAK,GAC3C,KAAK,WACF,SAAiB,MAAM,YAAY,KAAK,IACxC,SAAiB,MAAM,cAAc,KAAK;AACjD,OAAK,YAAY,cAAc,KAAK;AACpC,OAAK,YAAY,QAAQ,KAAK,qBAAqB,CAAC;;CAGtD,sBAAsC;EAEpC,MAAM,UAAU,kBAAkB,KAAK,KAAK;EAC5C,MAAM,aAAa,CAAC,MAAM,UAAU,MAAM,KAAK,KAAK,SAAS,CAAC,CAAC;AAC/D,MAAI,QACF,YAAW,KAAK,MAAM,IAAI,IAAI,QAAQ,GAAG,CAAC;EAE5C,IAAI,OAAO,WAAW,KAAK,IAAI;EAG/B,MAAM,SAAS,KAAK;AACpB,MAAI,OACF,KAAI,KAAK,SACP,SAAQ,KAAK,MAAM,WAAW,OAAO;OAChC;GAEL,MAAM,UAAU,OAAO,QAAQ,OAAO,IAAI,CAAC,QAAQ,QAAQ,IAAI,CAAC,MAAM;GACtE,MAAM,YAAY,OAAO,MAAM,KAAK,CAAC;GACrC,MAAM,mBAAmB;GACzB,MAAM,UAAU,QAAQ,SAAS,mBAC7B,GAAG,QAAQ,MAAM,GAAG,iBAAiB,CAAC,KACtC;GACJ,MAAM,SAAS,YAAY,IAAI,MAAM,IAAI,KAAK,UAAU,SAAS,GAAG;AACpE,WAAQ,KAAK,MAAM,WAAW,QAAQ,GAAG;;WAElC,KAAK,UACd,SAAQ,KAAK,MAAM,IAAI,IAAI;AAG7B,SAAO"}
|