@dmrlzz/davecode 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +33 -2
- package/bin/davecode.js +2 -0
- package/dist/ChatApp-FO5SABXB.js +893 -0
- package/dist/ChatApp-FO5SABXB.js.map +1 -0
- package/dist/RunnerView-DLVR7C22.js +18 -0
- package/dist/RunnerView-DLVR7C22.js.map +1 -0
- package/dist/accounts-MTMGYKEP.js +724 -0
- package/dist/accounts-MTMGYKEP.js.map +1 -0
- package/dist/budget-RBHCEDPP.js +115 -0
- package/dist/budget-RBHCEDPP.js.map +1 -0
- package/dist/chat-ZDXX3N6X.js +95 -0
- package/dist/chat-ZDXX3N6X.js.map +1 -0
- package/dist/chunk-23KTE7IK.js +114 -0
- package/dist/chunk-23KTE7IK.js.map +1 -0
- package/dist/chunk-7XVXEO7U.js +23 -0
- package/dist/chunk-7XVXEO7U.js.map +1 -0
- package/dist/chunk-BOLFK3BH.js +20 -0
- package/dist/chunk-BOLFK3BH.js.map +1 -0
- package/dist/chunk-C7FDRA3U.js +9087 -0
- package/dist/chunk-C7FDRA3U.js.map +1 -0
- package/dist/chunk-DYQ56FBT.js +47 -0
- package/dist/chunk-DYQ56FBT.js.map +1 -0
- package/dist/chunk-FRFSJMN4.js +79 -0
- package/dist/chunk-FRFSJMN4.js.map +1 -0
- package/dist/chunk-IC3TDONV.js +12 -0
- package/dist/chunk-IC3TDONV.js.map +1 -0
- package/dist/chunk-IVZHKUCD.js +180 -0
- package/dist/chunk-IVZHKUCD.js.map +1 -0
- package/dist/chunk-MSLLH5XP.js +213 -0
- package/dist/chunk-MSLLH5XP.js.map +1 -0
- package/dist/chunk-OXRMGR72.js +20 -0
- package/dist/chunk-OXRMGR72.js.map +1 -0
- package/dist/chunk-OYEYNDOO.js +200 -0
- package/dist/chunk-OYEYNDOO.js.map +1 -0
- package/dist/chunk-PIIJ3RSI.js +1610 -0
- package/dist/chunk-PIIJ3RSI.js.map +1 -0
- package/dist/chunk-SF5ZEAP4.js +105 -0
- package/dist/chunk-SF5ZEAP4.js.map +1 -0
- package/dist/chunk-VVCXGQMY.js +107 -0
- package/dist/chunk-VVCXGQMY.js.map +1 -0
- package/dist/config-FRMY5OHG.js +158 -0
- package/dist/config-FRMY5OHG.js.map +1 -0
- package/dist/dashboard/assets/Accounts-BR3ZfVk4.js +1 -0
- package/dist/dashboard/assets/Badge-B-qzNRrZ.js +55 -0
- package/dist/dashboard/assets/Callout-B9g_wPDi.js +1 -0
- package/dist/dashboard/assets/Field-BfabPEcY.js +1 -0
- package/dist/dashboard/assets/Goals-BsV-6m94.js +1 -0
- package/dist/dashboard/assets/Library-CEA9P1m-.js +1 -0
- package/dist/dashboard/assets/Markdown-ClRsddnB.js +69 -0
- package/dist/dashboard/assets/Panel-B9tSgX6l.js +1 -0
- package/dist/dashboard/assets/RoutesPage-Dh-BJ3ir.js +4 -0
- package/dist/dashboard/assets/Runner-C_e9l5l2.js +1 -0
- package/dist/dashboard/assets/Settings-C6Yc1Gjg.js +1 -0
- package/dist/dashboard/assets/Switch-CUpOJjUT.js +1 -0
- package/dist/dashboard/assets/Tasks-CTku0rR3.js +2 -0
- package/dist/dashboard/assets/Traffic-CE6wZSMr.js +1 -0
- package/dist/dashboard/assets/Workspace-CxFxvkA6.js +1 -0
- package/dist/dashboard/assets/arrow-up-DL3CuFOO.js +1 -0
- package/dist/dashboard/assets/forms-D6UIYgC5.js +1 -0
- package/dist/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/dashboard/assets/index-1iH716Xy.css +2 -0
- package/dist/dashboard/assets/index-BMWr9EKo.js +104 -0
- package/dist/dashboard/assets/task-errors-CE4oEr8H.js +1 -0
- package/dist/dashboard/assets/trash-CWqeZ7eg.js +1 -0
- package/dist/dashboard/favicon.svg +7 -0
- package/dist/dashboard/github.svg +1 -0
- package/dist/dashboard/index.html +31 -0
- package/dist/dist-ZPXEVBEC.js +357 -0
- package/dist/dist-ZPXEVBEC.js.map +1 -0
- package/dist/doctor-7RUD4D3A.js +279 -0
- package/dist/doctor-7RUD4D3A.js.map +1 -0
- package/dist/goal-JFRNPBSD.js +302 -0
- package/dist/goal-JFRNPBSD.js.map +1 -0
- package/dist/index.js +381 -0
- package/dist/index.js.map +1 -0
- package/dist/init-NFYNO3A3.js +194 -0
- package/dist/init-NFYNO3A3.js.map +1 -0
- package/dist/prompts-L37J72YW.js +215 -0
- package/dist/prompts-L37J72YW.js.map +1 -0
- package/dist/run-ARUPXFFB.js +376 -0
- package/dist/run-ARUPXFFB.js.map +1 -0
- package/dist/runner-state-QFU3LJVJ.js +17 -0
- package/dist/runner-state-QFU3LJVJ.js.map +1 -0
- package/dist/start-CFPOVIAR.js +285 -0
- package/dist/start-CFPOVIAR.js.map +1 -0
- package/dist/status-E4EACVE2.js +25 -0
- package/dist/status-E4EACVE2.js.map +1 -0
- package/dist/tasks-ZIK6BVEX.js +184 -0
- package/dist/tasks-ZIK6BVEX.js.map +1 -0
- package/package.json +68 -4
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Spinner
|
|
3
|
+
} from "./chunk-BOLFK3BH.js";
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider,
|
|
6
|
+
useTheme
|
|
7
|
+
} from "./chunk-OXRMGR72.js";
|
|
8
|
+
import {
|
|
9
|
+
formatTaskTree,
|
|
10
|
+
summaryLine
|
|
11
|
+
} from "./chunk-23KTE7IK.js";
|
|
12
|
+
import {
|
|
13
|
+
emptyUsage,
|
|
14
|
+
renderAccounts
|
|
15
|
+
} from "./chunk-MSLLH5XP.js";
|
|
16
|
+
import {
|
|
17
|
+
bar,
|
|
18
|
+
formatDuration,
|
|
19
|
+
formatTokens,
|
|
20
|
+
levelStyle,
|
|
21
|
+
padEnd,
|
|
22
|
+
percent,
|
|
23
|
+
visibleWidth
|
|
24
|
+
} from "./chunk-SF5ZEAP4.js";
|
|
25
|
+
import "./chunk-DYQ56FBT.js";
|
|
26
|
+
import "./chunk-VVCXGQMY.js";
|
|
27
|
+
import "./chunk-PIIJ3RSI.js";
|
|
28
|
+
import "./chunk-7XVXEO7U.js";
|
|
29
|
+
import "./chunk-C7FDRA3U.js";
|
|
30
|
+
|
|
31
|
+
// src/tui/ChatApp.tsx
|
|
32
|
+
import { Box as Box4, Static, useApp, useInput, usePaste, useStdout, useWindowSize } from "ink";
|
|
33
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
34
|
+
|
|
35
|
+
// src/tui/chat-state.ts
|
|
36
|
+
function initialChatState(model) {
|
|
37
|
+
return {
|
|
38
|
+
entries: [],
|
|
39
|
+
conversation: [],
|
|
40
|
+
model,
|
|
41
|
+
connection: "connecting",
|
|
42
|
+
lastFailovers: 0,
|
|
43
|
+
totalFailovers: 0,
|
|
44
|
+
tokens: { prompt: 0, completion: 0 },
|
|
45
|
+
epoch: 0,
|
|
46
|
+
history: [],
|
|
47
|
+
nextId: 1
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function estimateTokens(text) {
|
|
51
|
+
return Math.ceil(text.length / 4);
|
|
52
|
+
}
|
|
53
|
+
function push(state, kind, text, meta) {
|
|
54
|
+
const entry = { id: state.nextId, kind, text };
|
|
55
|
+
if (meta) entry.meta = meta;
|
|
56
|
+
return { ...state, entries: [...state.entries, entry], nextId: state.nextId + 1 };
|
|
57
|
+
}
|
|
58
|
+
function chatReducer(state, action) {
|
|
59
|
+
switch (action.type) {
|
|
60
|
+
case "banner":
|
|
61
|
+
return push(state, "banner", action.text);
|
|
62
|
+
case "info":
|
|
63
|
+
return push(state, "info", action.text);
|
|
64
|
+
case "error":
|
|
65
|
+
return push(state, "error", action.text);
|
|
66
|
+
case "user": {
|
|
67
|
+
const next = push(state, "user", action.text);
|
|
68
|
+
const history = state.history[state.history.length - 1] === action.text ? state.history : [...state.history, action.text].slice(-200);
|
|
69
|
+
return {
|
|
70
|
+
...next,
|
|
71
|
+
history,
|
|
72
|
+
conversation: [...state.conversation, { role: "user", content: action.text }]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
case "stream-start":
|
|
76
|
+
return {
|
|
77
|
+
...state,
|
|
78
|
+
streaming: { id: state.nextId, text: "", startedAt: action.now, model: state.model },
|
|
79
|
+
nextId: state.nextId + 1
|
|
80
|
+
};
|
|
81
|
+
case "stream-meta": {
|
|
82
|
+
if (!state.streaming) return state;
|
|
83
|
+
const streaming = { ...state.streaming };
|
|
84
|
+
if (action.accountId) streaming.accountId = action.accountId;
|
|
85
|
+
if (action.provider) streaming.provider = action.provider;
|
|
86
|
+
if (action.failovers !== void 0) streaming.failovers = action.failovers;
|
|
87
|
+
const lastAccount = action.accountId ? {
|
|
88
|
+
id: action.accountId,
|
|
89
|
+
...action.accountLabel ? { label: action.accountLabel } : {},
|
|
90
|
+
...action.provider ? { provider: action.provider } : {}
|
|
91
|
+
} : state.lastAccount;
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
streaming,
|
|
95
|
+
...lastAccount ? { lastAccount } : {},
|
|
96
|
+
...action.failovers !== void 0 ? {
|
|
97
|
+
lastFailovers: action.failovers,
|
|
98
|
+
totalFailovers: state.totalFailovers + action.failovers
|
|
99
|
+
} : {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
case "stream-delta":
|
|
103
|
+
if (!state.streaming) return state;
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
streaming: { ...state.streaming, text: state.streaming.text + action.text }
|
|
107
|
+
};
|
|
108
|
+
case "stream-end": {
|
|
109
|
+
const live = state.streaming;
|
|
110
|
+
if (!live) return state;
|
|
111
|
+
const prompt = action.usage?.prompt_tokens ?? estimateTokens(
|
|
112
|
+
state.conversation.map((m) => typeof m.content === "string" ? m.content : "").join("\n")
|
|
113
|
+
);
|
|
114
|
+
const completion = action.usage?.completion_tokens ?? estimateTokens(live.text);
|
|
115
|
+
const meta = {
|
|
116
|
+
model: live.model,
|
|
117
|
+
latencyMs: action.now - live.startedAt,
|
|
118
|
+
tokens: prompt + completion,
|
|
119
|
+
...action.usage ? {} : { estimated: true },
|
|
120
|
+
...live.accountId ? { accountId: live.accountId } : {},
|
|
121
|
+
...state.lastAccount?.id === live.accountId && state.lastAccount?.label ? { accountLabel: state.lastAccount.label } : {},
|
|
122
|
+
...live.provider ? { provider: live.provider } : {},
|
|
123
|
+
...live.failovers ? { failovers: live.failovers } : {},
|
|
124
|
+
...action.cancelled ? { cancelled: true } : {}
|
|
125
|
+
};
|
|
126
|
+
let next = { ...state };
|
|
127
|
+
delete next.streaming;
|
|
128
|
+
const answered = live.text.length > 0;
|
|
129
|
+
if (answered || action.cancelled) next = push(next, "assistant", live.text, meta);
|
|
130
|
+
if (action.error) next = push(next, "error", action.error);
|
|
131
|
+
if (answered && !action.cancelled && !action.error) {
|
|
132
|
+
next.conversation = [...next.conversation, { role: "assistant", content: live.text }];
|
|
133
|
+
} else if (next.conversation[next.conversation.length - 1]?.role === "user") {
|
|
134
|
+
next.conversation = next.conversation.slice(0, -1);
|
|
135
|
+
}
|
|
136
|
+
if (answered || action.usage) {
|
|
137
|
+
next.tokens = {
|
|
138
|
+
prompt: state.tokens.prompt + prompt,
|
|
139
|
+
completion: state.tokens.completion + completion
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
case "set-model":
|
|
145
|
+
return { ...state, model: action.model };
|
|
146
|
+
case "connection":
|
|
147
|
+
return state.connection === action.connection ? state : { ...state, connection: action.connection };
|
|
148
|
+
case "quota": {
|
|
149
|
+
if (state.lastAccount?.id !== action.accountId) return state;
|
|
150
|
+
const next = { ...state };
|
|
151
|
+
if (action.utilization === void 0) delete next.quota5h;
|
|
152
|
+
else next.quota5h = action.utilization;
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
case "clear": {
|
|
156
|
+
const next = {
|
|
157
|
+
...state,
|
|
158
|
+
entries: [],
|
|
159
|
+
conversation: [],
|
|
160
|
+
epoch: state.epoch + 1
|
|
161
|
+
};
|
|
162
|
+
delete next.streaming;
|
|
163
|
+
return next;
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
return state;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/tui/components/InputBox.tsx
|
|
171
|
+
import { Box, Text } from "ink";
|
|
172
|
+
|
|
173
|
+
// src/tui/editor.ts
|
|
174
|
+
var emptyEditor = { chars: [], cursor: 0 };
|
|
175
|
+
function fromText(text) {
|
|
176
|
+
const chars = [...text];
|
|
177
|
+
return { chars, cursor: chars.length };
|
|
178
|
+
}
|
|
179
|
+
function textOf(state) {
|
|
180
|
+
return state.chars.join("");
|
|
181
|
+
}
|
|
182
|
+
function normalizeInput(text) {
|
|
183
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, "");
|
|
184
|
+
}
|
|
185
|
+
function insert(state, text) {
|
|
186
|
+
const add = [...normalizeInput(text)];
|
|
187
|
+
if (add.length === 0) return state;
|
|
188
|
+
const chars = [...state.chars.slice(0, state.cursor), ...add, ...state.chars.slice(state.cursor)];
|
|
189
|
+
return { chars, cursor: state.cursor + add.length };
|
|
190
|
+
}
|
|
191
|
+
function backspace(state) {
|
|
192
|
+
if (state.cursor === 0) return state;
|
|
193
|
+
const chars = [...state.chars.slice(0, state.cursor - 1), ...state.chars.slice(state.cursor)];
|
|
194
|
+
return { chars, cursor: state.cursor - 1 };
|
|
195
|
+
}
|
|
196
|
+
function deleteForward(state) {
|
|
197
|
+
if (state.cursor >= state.chars.length) return state;
|
|
198
|
+
const chars = [...state.chars.slice(0, state.cursor), ...state.chars.slice(state.cursor + 1)];
|
|
199
|
+
return { chars, cursor: state.cursor };
|
|
200
|
+
}
|
|
201
|
+
function left(state) {
|
|
202
|
+
return { ...state, cursor: Math.max(0, state.cursor - 1) };
|
|
203
|
+
}
|
|
204
|
+
function right(state) {
|
|
205
|
+
return { ...state, cursor: Math.min(state.chars.length, state.cursor + 1) };
|
|
206
|
+
}
|
|
207
|
+
function position(state) {
|
|
208
|
+
let line = 0;
|
|
209
|
+
let column = 0;
|
|
210
|
+
for (let i = 0; i < state.cursor; i++) {
|
|
211
|
+
if (state.chars[i] === "\n") {
|
|
212
|
+
line++;
|
|
213
|
+
column = 0;
|
|
214
|
+
} else {
|
|
215
|
+
column++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { line, column };
|
|
219
|
+
}
|
|
220
|
+
function lines(state) {
|
|
221
|
+
const out = [[]];
|
|
222
|
+
for (const ch of state.chars) {
|
|
223
|
+
if (ch === "\n") out.push([]);
|
|
224
|
+
else out[out.length - 1].push(ch);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
function offsetOf(all, line, column) {
|
|
229
|
+
let offset = 0;
|
|
230
|
+
for (let i = 0; i < line; i++) offset += all[i].length + 1;
|
|
231
|
+
return offset + Math.min(column, all[line].length);
|
|
232
|
+
}
|
|
233
|
+
function up(state) {
|
|
234
|
+
const { line, column } = position(state);
|
|
235
|
+
if (line === 0) return void 0;
|
|
236
|
+
return { ...state, cursor: offsetOf(lines(state), line - 1, column) };
|
|
237
|
+
}
|
|
238
|
+
function down(state) {
|
|
239
|
+
const all = lines(state);
|
|
240
|
+
const { line, column } = position(state);
|
|
241
|
+
if (line >= all.length - 1) return void 0;
|
|
242
|
+
return { ...state, cursor: offsetOf(all, line + 1, column) };
|
|
243
|
+
}
|
|
244
|
+
function lineStart(state) {
|
|
245
|
+
const { line } = position(state);
|
|
246
|
+
return { ...state, cursor: offsetOf(lines(state), line, 0) };
|
|
247
|
+
}
|
|
248
|
+
function lineEnd(state) {
|
|
249
|
+
const all = lines(state);
|
|
250
|
+
const { line } = position(state);
|
|
251
|
+
return { ...state, cursor: offsetOf(all, line, all[line].length) };
|
|
252
|
+
}
|
|
253
|
+
function killToLineStart(state) {
|
|
254
|
+
const start = lineStart(state).cursor;
|
|
255
|
+
if (start === state.cursor) return state;
|
|
256
|
+
return {
|
|
257
|
+
chars: [...state.chars.slice(0, start), ...state.chars.slice(state.cursor)],
|
|
258
|
+
cursor: start
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function killToLineEnd(state) {
|
|
262
|
+
const end = lineEnd(state).cursor;
|
|
263
|
+
return {
|
|
264
|
+
chars: [...state.chars.slice(0, state.cursor), ...state.chars.slice(end)],
|
|
265
|
+
cursor: state.cursor
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function deleteWordBack(state) {
|
|
269
|
+
let i = state.cursor;
|
|
270
|
+
while (i > 0 && /[ \t]/.test(state.chars[i - 1])) i--;
|
|
271
|
+
while (i > 0 && !/[\s]/.test(state.chars[i - 1])) i--;
|
|
272
|
+
if (i === state.cursor) return backspace(state);
|
|
273
|
+
return { chars: [...state.chars.slice(0, i), ...state.chars.slice(state.cursor)], cursor: i };
|
|
274
|
+
}
|
|
275
|
+
function continuesLine(state) {
|
|
276
|
+
return state.cursor > 0 && state.chars[state.cursor - 1] === "\\";
|
|
277
|
+
}
|
|
278
|
+
function newlineAfterBackslash(state) {
|
|
279
|
+
return insert(backspace(state), "\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/tui/components/InputBox.tsx
|
|
283
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
284
|
+
function cursorParts(line, column) {
|
|
285
|
+
if (column === void 0) return { before: line.join(""), at: "", after: "" };
|
|
286
|
+
return {
|
|
287
|
+
before: line.slice(0, column).join(""),
|
|
288
|
+
at: line[column] ?? " ",
|
|
289
|
+
after: line.slice(column + 1).join("")
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function InputBox({
|
|
293
|
+
editor,
|
|
294
|
+
width,
|
|
295
|
+
placeholder,
|
|
296
|
+
disabled
|
|
297
|
+
}) {
|
|
298
|
+
const theme = useTheme();
|
|
299
|
+
const all = lines(editor);
|
|
300
|
+
const cursor = position(editor);
|
|
301
|
+
const empty = editor.chars.length === 0;
|
|
302
|
+
return /* @__PURE__ */ jsx(
|
|
303
|
+
Box,
|
|
304
|
+
{
|
|
305
|
+
borderStyle: theme.unicode ? "round" : "classic",
|
|
306
|
+
borderDimColor: theme.ink.dim,
|
|
307
|
+
flexDirection: "column",
|
|
308
|
+
paddingX: 1,
|
|
309
|
+
width,
|
|
310
|
+
children: all.map((line, i) => {
|
|
311
|
+
const parts = cursorParts(line, !disabled && i === cursor.line ? cursor.column : void 0);
|
|
312
|
+
return (
|
|
313
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: lines have no identity beyond position
|
|
314
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
315
|
+
/* @__PURE__ */ jsx(Box, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: i === 0 ? theme.ink.accent : void 0, children: i === 0 ? theme.glyph.prompt : "" }) }),
|
|
316
|
+
empty && i === 0 ? /* @__PURE__ */ jsxs(Text, { children: [
|
|
317
|
+
disabled ? null : /* @__PURE__ */ jsx(Text, { inverse: true, children: " " }),
|
|
318
|
+
/* @__PURE__ */ jsx(Text, { dimColor: theme.ink.dim, children: placeholder })
|
|
319
|
+
] }) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
320
|
+
parts.before,
|
|
321
|
+
parts.at ? /* @__PURE__ */ jsx(Text, { inverse: true, children: parts.at }) : null,
|
|
322
|
+
parts.after
|
|
323
|
+
] })
|
|
324
|
+
] }, i)
|
|
325
|
+
);
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
function SlashHints({ commands, width }) {
|
|
331
|
+
const theme = useTheme();
|
|
332
|
+
if (commands.length === 0) return null;
|
|
333
|
+
const nameWidth = Math.max(...commands.map((c) => c.name.length + (c.args ? c.args.length + 1 : 0))) + 3;
|
|
334
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 2, width, children: commands.map((c, i) => /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
335
|
+
/* @__PURE__ */ jsx(Text, { color: i === 0 ? theme.ink.accent : void 0, children: `/${c.name}${c.args ? ` ${c.args}` : ""}`.padEnd(nameWidth) }),
|
|
336
|
+
/* @__PURE__ */ jsx(Text, { dimColor: theme.ink.dim, children: c.description })
|
|
337
|
+
] }, c.name)) });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/tui/components/StatusLine.tsx
|
|
341
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
342
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
343
|
+
function statusSegments(theme, state, info, width) {
|
|
344
|
+
const sep = theme.dim(" \xB7 ");
|
|
345
|
+
const left2 = [theme.accent(state.model)];
|
|
346
|
+
if (state.lastAccount) {
|
|
347
|
+
const name = state.lastAccount.label ?? state.lastAccount.id;
|
|
348
|
+
left2.push(
|
|
349
|
+
`${name}${state.lastAccount.provider ? theme.dim(` (${state.lastAccount.provider})`) : ""}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const failovers = state.totalFailovers > 0 ? theme.warn(`${state.lastFailovers}/${state.totalFailovers} failovers`) : theme.dim("0 failovers");
|
|
353
|
+
left2.push(failovers);
|
|
354
|
+
const arrows = theme.unicode ? ["\u2191", "\u2193"] : ["in", "out"];
|
|
355
|
+
left2.push(
|
|
356
|
+
theme.dim(
|
|
357
|
+
`${formatTokens(state.tokens.prompt)}${arrows[0]} ${formatTokens(state.tokens.completion)}${arrows[1]}`
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
if (state.quota5h !== void 0) {
|
|
361
|
+
left2.push(
|
|
362
|
+
`${theme.dim("5h")} ${bar(theme, state.quota5h, 6)} ${levelStyle(theme, state.quota5h)(percent(state.quota5h))}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const dot = theme.glyph.dot;
|
|
366
|
+
let right2;
|
|
367
|
+
if (info.notice) right2 = theme.warn(info.notice);
|
|
368
|
+
else if (state.connection === "online") {
|
|
369
|
+
right2 = `${theme.ok(dot)} ${theme.dim(info.inProcess ? `${info.where}` : "connected")}`;
|
|
370
|
+
} else if (state.connection === "connecting") right2 = theme.dim(`${dot} connecting`);
|
|
371
|
+
else right2 = theme.error(`${dot} offline`);
|
|
372
|
+
const priorities = [0, 1, 2, 4, 3];
|
|
373
|
+
let kept = left2.map((_, i) => i);
|
|
374
|
+
const render = () => kept.map((i) => left2[i]).join(sep);
|
|
375
|
+
while (kept.length > 1 && visibleWidth(render()) + visibleWidth(right2) + 2 > width) {
|
|
376
|
+
const drop = [...priorities].reverse().find((p) => kept.includes(p) && p !== 0);
|
|
377
|
+
if (drop === void 0) break;
|
|
378
|
+
kept = kept.filter((i) => i !== drop);
|
|
379
|
+
}
|
|
380
|
+
return { left: render(), right: right2 };
|
|
381
|
+
}
|
|
382
|
+
function StatusLine({
|
|
383
|
+
state,
|
|
384
|
+
info,
|
|
385
|
+
width
|
|
386
|
+
}) {
|
|
387
|
+
const theme = useTheme();
|
|
388
|
+
const { left: left2, right: right2 } = statusSegments(theme, state, info, width - 2);
|
|
389
|
+
return /* @__PURE__ */ jsxs2(Box2, { width, justifyContent: "space-between", paddingX: 1, children: [
|
|
390
|
+
/* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", children: left2 }),
|
|
391
|
+
/* @__PURE__ */ jsx2(Text2, { wrap: "truncate-start", children: right2 })
|
|
392
|
+
] });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/tui/components/Transcript.tsx
|
|
396
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
397
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
398
|
+
function metaLine(meta) {
|
|
399
|
+
const parts = [];
|
|
400
|
+
if (meta.accountLabel ?? meta.accountId) {
|
|
401
|
+
parts.push([meta.provider, meta.accountLabel ?? meta.accountId].filter(Boolean).join(" \xB7 "));
|
|
402
|
+
}
|
|
403
|
+
if (meta.model) parts.push(meta.model);
|
|
404
|
+
if (meta.tokens !== void 0)
|
|
405
|
+
parts.push(`${meta.estimated ? "~" : ""}${formatTokens(meta.tokens)} tok`);
|
|
406
|
+
if (meta.latencyMs !== void 0) parts.push(`${(meta.latencyMs / 1e3).toFixed(1)}s`);
|
|
407
|
+
if (meta.failovers) parts.push(`${meta.failovers} failover${meta.failovers === 1 ? "" : "s"}`);
|
|
408
|
+
if (meta.cancelled) parts.push("cancelled");
|
|
409
|
+
return parts.join(" \xB7 ");
|
|
410
|
+
}
|
|
411
|
+
function Marker({ color, glyph }) {
|
|
412
|
+
return /* @__PURE__ */ jsx3(Box3, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx3(Text3, { color, children: glyph }) });
|
|
413
|
+
}
|
|
414
|
+
function EntryView({ entry }) {
|
|
415
|
+
const theme = useTheme();
|
|
416
|
+
switch (entry.kind) {
|
|
417
|
+
case "banner":
|
|
418
|
+
return /* @__PURE__ */ jsx3(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsx3(Text3, { children: entry.text }) });
|
|
419
|
+
case "user":
|
|
420
|
+
return /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
|
|
421
|
+
/* @__PURE__ */ jsx3(Marker, { color: theme.ink.accent, glyph: theme.glyph.prompt }),
|
|
422
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: theme.ink.dim, children: entry.text })
|
|
423
|
+
] });
|
|
424
|
+
case "assistant":
|
|
425
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
426
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
427
|
+
/* @__PURE__ */ jsx3(Marker, { color: theme.ink.accent, glyph: theme.glyph.dot }),
|
|
428
|
+
/* @__PURE__ */ jsx3(Text3, { children: entry.text || (entry.meta?.cancelled ? "(cancelled before any output)" : "") })
|
|
429
|
+
] }),
|
|
430
|
+
entry.meta ? /* @__PURE__ */ jsx3(Box3, { paddingLeft: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: theme.ink.dim, children: metaLine(entry.meta) }) }) : null
|
|
431
|
+
] });
|
|
432
|
+
case "error":
|
|
433
|
+
return /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
|
|
434
|
+
/* @__PURE__ */ jsx3(Marker, { color: theme.ink.error, glyph: theme.glyph.fail }),
|
|
435
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme.ink.error, children: entry.text })
|
|
436
|
+
] });
|
|
437
|
+
default:
|
|
438
|
+
return /* @__PURE__ */ jsx3(Box3, { marginBottom: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx3(Text3, { children: entry.text }) });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function StreamingView({ streaming, now }) {
|
|
442
|
+
const theme = useTheme();
|
|
443
|
+
const seconds = Math.max(0, (now - streaming.startedAt) / 1e3).toFixed(0);
|
|
444
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
445
|
+
streaming.text ? /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
446
|
+
/* @__PURE__ */ jsx3(Marker, { color: theme.ink.accent, glyph: theme.glyph.dot }),
|
|
447
|
+
/* @__PURE__ */ jsx3(Text3, { children: streaming.text })
|
|
448
|
+
] }) : null,
|
|
449
|
+
/* @__PURE__ */ jsxs3(Box3, { paddingLeft: streaming.text ? 2 : 0, children: [
|
|
450
|
+
/* @__PURE__ */ jsx3(Spinner, {}),
|
|
451
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: theme.ink.dim, children: [
|
|
452
|
+
" ",
|
|
453
|
+
streaming.text ? "streaming" : "thinking",
|
|
454
|
+
" ",
|
|
455
|
+
seconds,
|
|
456
|
+
"s \xB7 esc to cancel"
|
|
457
|
+
] })
|
|
458
|
+
] })
|
|
459
|
+
] });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/tui/slash.ts
|
|
463
|
+
var SLASH_COMMANDS = [
|
|
464
|
+
{ name: "model", args: "[id]", description: "show models or switch, e.g. /model davecode/auto" },
|
|
465
|
+
{ name: "route", description: "routes and their failover targets" },
|
|
466
|
+
{ name: "accounts", description: "accounts with 5h quota usage" },
|
|
467
|
+
{ name: "tasks", description: "the project task graph" },
|
|
468
|
+
{ name: "status", description: "gateway health and runner state" },
|
|
469
|
+
{ name: "clear", description: "clear the transcript and start a new conversation" },
|
|
470
|
+
{ name: "help", description: "commands and keyboard shortcuts", aliases: ["?"] },
|
|
471
|
+
{ name: "exit", description: "quit DaveCode", aliases: ["quit", "q"] }
|
|
472
|
+
];
|
|
473
|
+
function parseSlash(input) {
|
|
474
|
+
const text = input.trim();
|
|
475
|
+
if (!text.startsWith("/") || text.startsWith("//")) return { kind: "none" };
|
|
476
|
+
const [head = "", ...rest] = text.slice(1).split(/\s+/);
|
|
477
|
+
const name = head.toLowerCase();
|
|
478
|
+
if (!name) return { kind: "unknown", name: "" };
|
|
479
|
+
const command = SLASH_COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
|
|
480
|
+
if (!command) return { kind: "unknown", name };
|
|
481
|
+
return { kind: "command", command, args: rest.join(" ").trim() };
|
|
482
|
+
}
|
|
483
|
+
function slashSuggestions(input) {
|
|
484
|
+
if (!input.startsWith("/") || /\s/.test(input)) return [];
|
|
485
|
+
const prefix = input.slice(1).toLowerCase();
|
|
486
|
+
return SLASH_COMMANDS.filter(
|
|
487
|
+
(c) => c.name.startsWith(prefix) || c.aliases?.some((a) => a.startsWith(prefix) && prefix)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
function completeSlash(input) {
|
|
491
|
+
const [first] = slashSuggestions(input);
|
|
492
|
+
if (!first) return void 0;
|
|
493
|
+
return `/${first.name}${first.args ? " " : ""}`;
|
|
494
|
+
}
|
|
495
|
+
var KEY_HINTS = [
|
|
496
|
+
["Enter", "send"],
|
|
497
|
+
["Shift+Enter, Ctrl+J or \\ Enter", "new line"],
|
|
498
|
+
["\u2191 / \u2193", "move between lines, then through history"],
|
|
499
|
+
["Tab", "complete a slash command"],
|
|
500
|
+
["Esc", "cancel the response in flight"],
|
|
501
|
+
["Ctrl+U / Ctrl+K / Ctrl+W", "delete to line start / line end / previous word"],
|
|
502
|
+
["Ctrl+C twice", "exit"]
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
// src/tui/slash-actions.ts
|
|
506
|
+
function helpText(theme) {
|
|
507
|
+
const width = Math.max(...SLASH_COMMANDS.map((c) => c.name.length + (c.args?.length ?? 0))) + 4;
|
|
508
|
+
const commands = SLASH_COMMANDS.map(
|
|
509
|
+
(c) => `${theme.accent(padEnd(`/${c.name}${c.args ? ` ${c.args}` : ""}`, width))}${theme.dim(c.description)}`
|
|
510
|
+
);
|
|
511
|
+
const keyWidth = Math.max(...KEY_HINTS.map(([k]) => k.length)) + 2;
|
|
512
|
+
const keys = KEY_HINTS.map(([k, v]) => `${padEnd(k, keyWidth)}${theme.dim(v)}`);
|
|
513
|
+
return [theme.bold("Commands"), ...commands, "", theme.bold("Keys"), ...keys].join("\n");
|
|
514
|
+
}
|
|
515
|
+
async function modelsText(theme, backend, current) {
|
|
516
|
+
const models = await backend.models();
|
|
517
|
+
const ids = models.map((m) => m.id).sort((a, b) => {
|
|
518
|
+
const da = a.startsWith("davecode/") ? 0 : 1;
|
|
519
|
+
const db = b.startsWith("davecode/") ? 0 : 1;
|
|
520
|
+
return da - db || a.localeCompare(b);
|
|
521
|
+
});
|
|
522
|
+
if (ids.length === 0)
|
|
523
|
+
return theme.dim("No models yet. Add an account with `davecode accounts add`.");
|
|
524
|
+
const shown = ids.slice(0, 40);
|
|
525
|
+
const lines2 = shown.map(
|
|
526
|
+
(id) => id === current ? `${theme.accent(theme.glyph.dot)} ${theme.accent(id)}` : ` ${id}`
|
|
527
|
+
);
|
|
528
|
+
if (ids.length > shown.length) lines2.push(theme.dim(` \u2026 and ${ids.length - shown.length} more`));
|
|
529
|
+
lines2.push("", theme.dim("Switch with /model <id>"));
|
|
530
|
+
return lines2.join("\n");
|
|
531
|
+
}
|
|
532
|
+
async function routesText(theme, backend) {
|
|
533
|
+
const { defaultRoute, routes } = await backend.routes();
|
|
534
|
+
if (routes.length === 0) {
|
|
535
|
+
return [
|
|
536
|
+
`${theme.dim("No routes configured.")} davecode/${defaultRoute} uses every account with a default model.`,
|
|
537
|
+
theme.dim("Define routes under routing.routes in ~/.davecode/config.json.")
|
|
538
|
+
].join("\n");
|
|
539
|
+
}
|
|
540
|
+
const lines2 = [];
|
|
541
|
+
for (const route of routes) {
|
|
542
|
+
const mark = route.name === defaultRoute ? theme.dim(" (default)") : "";
|
|
543
|
+
lines2.push(
|
|
544
|
+
`${theme.accent(`davecode/${route.name}`)}${mark}${route.description ? theme.dim(` ${route.description}`) : ""}`
|
|
545
|
+
);
|
|
546
|
+
route.targets.forEach((t, i) => {
|
|
547
|
+
lines2.push(
|
|
548
|
+
` ${theme.dim(`${i + 1}.`)} ${t.provider}/${t.model}${t.accountId ? theme.dim(` @${t.accountId}`) : ""}`
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return lines2.join("\n");
|
|
553
|
+
}
|
|
554
|
+
async function accountsText(theme, backend, columns) {
|
|
555
|
+
const [accounts, usage] = await Promise.all([backend.accounts(), backend.usage()]);
|
|
556
|
+
if (accounts.length === 0)
|
|
557
|
+
return theme.dim("No accounts yet. Add one with `davecode accounts add`.");
|
|
558
|
+
const byId = new Map(usage.map((u) => [u.accountId, u]));
|
|
559
|
+
return renderAccounts(
|
|
560
|
+
theme,
|
|
561
|
+
accounts.map((account) => ({ account, usage: byId.get(account.id) ?? emptyUsage(account.id) })),
|
|
562
|
+
columns
|
|
563
|
+
).join("\n");
|
|
564
|
+
}
|
|
565
|
+
async function tasksText(theme, backend, columns) {
|
|
566
|
+
const { project, graph } = await backend.tasks();
|
|
567
|
+
if (!project)
|
|
568
|
+
return theme.dim("No project brain is being served. Start DaveCode inside a repository.");
|
|
569
|
+
if (graph.tasks.length === 0) {
|
|
570
|
+
return theme.dim(`${project.name}: no tasks yet. Add one with \`davecode tasks add\`.`);
|
|
571
|
+
}
|
|
572
|
+
return [
|
|
573
|
+
`${theme.bold(project.name)} ${summaryLine(theme, graph)}`,
|
|
574
|
+
"",
|
|
575
|
+
...formatTaskTree(theme, graph, { width: columns })
|
|
576
|
+
].join("\n");
|
|
577
|
+
}
|
|
578
|
+
async function statusText(theme, backend) {
|
|
579
|
+
const health = await backend.health();
|
|
580
|
+
if (!health) return theme.error(`Gateway at ${backend.url} is not answering.`);
|
|
581
|
+
const runner = await backend.runner().catch(() => void 0);
|
|
582
|
+
const flags = Object.entries(health.experimental).filter(([, on]) => on).map(([k]) => k);
|
|
583
|
+
return [
|
|
584
|
+
`${theme.ok(theme.glyph.dot)} ${backend.url}${backend.inProcess ? theme.dim(" (in-process)") : ""} ${theme.dim(`v${health.version} \xB7 up ${formatDuration(health.uptimeSec)}`)}`,
|
|
585
|
+
`${theme.dim("runner")} ${runner ? runner.state : "unknown"}${runner?.taskId ? theme.dim(` \xB7 ${runner.taskId}`) : ""}`,
|
|
586
|
+
`${theme.dim("experimental")} ${flags.length > 0 ? theme.warn(flags.join(", ")) : "none"}`
|
|
587
|
+
].join("\n");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/tui/ChatApp.tsx
|
|
591
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
592
|
+
var PLACEHOLDER = "Ask anything \xB7 / for commands \xB7 Shift+Enter or Ctrl+J for a new line";
|
|
593
|
+
var EXIT_WINDOW_MS = 2e3;
|
|
594
|
+
function errorText(err) {
|
|
595
|
+
return err instanceof Error ? err.message : String(err);
|
|
596
|
+
}
|
|
597
|
+
function Chat({ backend, model, version, pollMs = 1e4, now = Date.now }) {
|
|
598
|
+
const theme = useTheme();
|
|
599
|
+
const { exit } = useApp();
|
|
600
|
+
const { write } = useStdout();
|
|
601
|
+
const { columns } = useWindowSize();
|
|
602
|
+
const width = Math.max(20, columns);
|
|
603
|
+
const [state, dispatch] = useReducer(chatReducer, model, initialChatState);
|
|
604
|
+
const [editor, setEditor] = useState(emptyEditor);
|
|
605
|
+
const [historyIndex, setHistoryIndex] = useState();
|
|
606
|
+
const [notice, setNotice] = useState();
|
|
607
|
+
const [clock, setClock] = useState(now());
|
|
608
|
+
const abortRef = useRef(void 0);
|
|
609
|
+
const exitArmedAt = useRef(void 0);
|
|
610
|
+
const labels = useRef(/* @__PURE__ */ new Map());
|
|
611
|
+
const stateRef = useRef(state);
|
|
612
|
+
stateRef.current = state;
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
const where = backend.inProcess ? `${backend.url} (in-process)` : backend.url;
|
|
615
|
+
dispatch({
|
|
616
|
+
type: "banner",
|
|
617
|
+
text: `${theme.bold(theme.accent("DaveCode"))} ${theme.dim(`v${version}`)}
|
|
618
|
+
${theme.dim(
|
|
619
|
+
`model ${model} \xB7 gateway ${where} \xB7 /help for commands`
|
|
620
|
+
)}`
|
|
621
|
+
});
|
|
622
|
+
let alive = true;
|
|
623
|
+
const check = async () => {
|
|
624
|
+
const health = await backend.health();
|
|
625
|
+
if (alive) dispatch({ type: "connection", connection: health ? "online" : "offline" });
|
|
626
|
+
};
|
|
627
|
+
void check();
|
|
628
|
+
backend.accounts().then((accounts) => {
|
|
629
|
+
for (const a of accounts) labels.current.set(a.id, a.label);
|
|
630
|
+
}).catch(() => {
|
|
631
|
+
});
|
|
632
|
+
const timer = setInterval(() => void check(), pollMs);
|
|
633
|
+
return () => {
|
|
634
|
+
alive = false;
|
|
635
|
+
clearInterval(timer);
|
|
636
|
+
abortRef.current?.abort();
|
|
637
|
+
};
|
|
638
|
+
}, [backend, model, pollMs, theme, version]);
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (!state.streaming) return;
|
|
641
|
+
const timer = setInterval(() => setClock(now()), 250);
|
|
642
|
+
return () => clearInterval(timer);
|
|
643
|
+
}, [state.streaming, now]);
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
if (!notice) return;
|
|
646
|
+
const timer = setTimeout(() => setNotice(void 0), EXIT_WINDOW_MS);
|
|
647
|
+
return () => clearTimeout(timer);
|
|
648
|
+
}, [notice]);
|
|
649
|
+
const refreshQuota = useCallback(
|
|
650
|
+
async (accountId) => {
|
|
651
|
+
try {
|
|
652
|
+
const usage = (await backend.usage()).find((u) => u.accountId === accountId);
|
|
653
|
+
const window = usage?.windows["5h"];
|
|
654
|
+
const limited = window && (window.tokenLimit !== void 0 || window.requestLimit !== void 0);
|
|
655
|
+
dispatch({
|
|
656
|
+
type: "quota",
|
|
657
|
+
accountId,
|
|
658
|
+
utilization: limited ? window.utilization : void 0
|
|
659
|
+
});
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
[backend]
|
|
664
|
+
);
|
|
665
|
+
const send = useCallback(
|
|
666
|
+
async (text) => {
|
|
667
|
+
dispatch({ type: "user", text });
|
|
668
|
+
const conversation = [
|
|
669
|
+
...stateRef.current.conversation,
|
|
670
|
+
{ role: "user", content: text }
|
|
671
|
+
];
|
|
672
|
+
const controller = new AbortController();
|
|
673
|
+
abortRef.current = controller;
|
|
674
|
+
dispatch({ type: "stream-start", now: now() });
|
|
675
|
+
let usage;
|
|
676
|
+
let accountId;
|
|
677
|
+
try {
|
|
678
|
+
const { meta, chunks } = await backend.stream(
|
|
679
|
+
{ model: stateRef.current.model, messages: conversation },
|
|
680
|
+
controller.signal
|
|
681
|
+
);
|
|
682
|
+
accountId = meta.accountId;
|
|
683
|
+
dispatch({
|
|
684
|
+
type: "stream-meta",
|
|
685
|
+
failovers: meta.failovers,
|
|
686
|
+
...meta.accountId ? { accountId: meta.accountId } : {},
|
|
687
|
+
...meta.provider ? { provider: meta.provider } : {},
|
|
688
|
+
...meta.accountId && labels.current.has(meta.accountId) ? { accountLabel: labels.current.get(meta.accountId) } : {}
|
|
689
|
+
});
|
|
690
|
+
dispatch({ type: "connection", connection: "online" });
|
|
691
|
+
for await (const chunk of chunks) {
|
|
692
|
+
if (controller.signal.aborted) break;
|
|
693
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
694
|
+
if (typeof delta === "string" && delta) dispatch({ type: "stream-delta", text: delta });
|
|
695
|
+
if (chunk.usage) usage = chunk.usage;
|
|
696
|
+
}
|
|
697
|
+
dispatch({
|
|
698
|
+
type: "stream-end",
|
|
699
|
+
now: now(),
|
|
700
|
+
...usage ? { usage } : {},
|
|
701
|
+
...controller.signal.aborted ? { cancelled: true } : {}
|
|
702
|
+
});
|
|
703
|
+
} catch (err) {
|
|
704
|
+
if (controller.signal.aborted) {
|
|
705
|
+
dispatch({ type: "stream-end", now: now(), cancelled: true });
|
|
706
|
+
} else {
|
|
707
|
+
const status = err.status;
|
|
708
|
+
if (status === 0) dispatch({ type: "connection", connection: "offline" });
|
|
709
|
+
dispatch({ type: "stream-end", now: now(), error: errorText(err) });
|
|
710
|
+
}
|
|
711
|
+
} finally {
|
|
712
|
+
if (abortRef.current === controller) abortRef.current = void 0;
|
|
713
|
+
}
|
|
714
|
+
if (accountId) void refreshQuota(accountId);
|
|
715
|
+
},
|
|
716
|
+
[backend, now, refreshQuota]
|
|
717
|
+
);
|
|
718
|
+
const runSlash = useCallback(
|
|
719
|
+
async (input) => {
|
|
720
|
+
const parsed = parseSlash(input);
|
|
721
|
+
if (parsed.kind === "unknown") {
|
|
722
|
+
dispatch({
|
|
723
|
+
type: "error",
|
|
724
|
+
text: `Unknown command /${parsed.name}. Type /help for the list.`
|
|
725
|
+
});
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (parsed.kind !== "command") return;
|
|
729
|
+
const { command, args } = parsed;
|
|
730
|
+
try {
|
|
731
|
+
switch (command.name) {
|
|
732
|
+
case "help":
|
|
733
|
+
dispatch({ type: "info", text: helpText(theme) });
|
|
734
|
+
break;
|
|
735
|
+
case "exit":
|
|
736
|
+
abortRef.current?.abort();
|
|
737
|
+
exit();
|
|
738
|
+
break;
|
|
739
|
+
case "clear":
|
|
740
|
+
abortRef.current?.abort();
|
|
741
|
+
write("\x1B[2J\x1B[3J\x1B[H");
|
|
742
|
+
dispatch({ type: "clear" });
|
|
743
|
+
break;
|
|
744
|
+
case "model":
|
|
745
|
+
if (args) {
|
|
746
|
+
dispatch({ type: "set-model", model: args });
|
|
747
|
+
dispatch({ type: "info", text: `Model set to ${theme.accent(args)}` });
|
|
748
|
+
} else {
|
|
749
|
+
dispatch({
|
|
750
|
+
type: "info",
|
|
751
|
+
text: await modelsText(theme, backend, stateRef.current.model)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
case "route":
|
|
756
|
+
dispatch({ type: "info", text: await routesText(theme, backend) });
|
|
757
|
+
break;
|
|
758
|
+
case "accounts": {
|
|
759
|
+
const accounts = await backend.accounts();
|
|
760
|
+
for (const a of accounts) labels.current.set(a.id, a.label);
|
|
761
|
+
dispatch({ type: "info", text: await accountsText(theme, backend, width - 4) });
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "tasks":
|
|
765
|
+
dispatch({ type: "info", text: await tasksText(theme, backend, width - 4) });
|
|
766
|
+
break;
|
|
767
|
+
case "status":
|
|
768
|
+
dispatch({ type: "info", text: await statusText(theme, backend) });
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
} catch (err) {
|
|
772
|
+
dispatch({ type: "error", text: `/${command.name} failed: ${errorText(err)}` });
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
[backend, exit, theme, width, write]
|
|
776
|
+
);
|
|
777
|
+
const submit = useCallback(() => {
|
|
778
|
+
const text = textOf(editor).trim();
|
|
779
|
+
if (!text) return;
|
|
780
|
+
if (parseSlash(text).kind !== "none") {
|
|
781
|
+
setEditor(emptyEditor);
|
|
782
|
+
setHistoryIndex(void 0);
|
|
783
|
+
void runSlash(text);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (stateRef.current.streaming) {
|
|
787
|
+
setNotice("Wait for the reply, or press Esc to cancel it");
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
setEditor(emptyEditor);
|
|
791
|
+
setHistoryIndex(void 0);
|
|
792
|
+
void send(text);
|
|
793
|
+
}, [editor, runSlash, send]);
|
|
794
|
+
const recall = (direction) => {
|
|
795
|
+
const { history } = stateRef.current;
|
|
796
|
+
if (history.length === 0) return;
|
|
797
|
+
const current = historyIndex ?? history.length;
|
|
798
|
+
const next = Math.min(history.length, Math.max(0, current + direction));
|
|
799
|
+
setHistoryIndex(next === history.length ? void 0 : next);
|
|
800
|
+
setEditor(next === history.length ? emptyEditor : fromText(history[next]));
|
|
801
|
+
};
|
|
802
|
+
useInput((input, key) => {
|
|
803
|
+
if (key.ctrl && input === "c") {
|
|
804
|
+
const armed = exitArmedAt.current !== void 0 && now() - exitArmedAt.current < EXIT_WINDOW_MS;
|
|
805
|
+
if (armed) {
|
|
806
|
+
abortRef.current?.abort();
|
|
807
|
+
exit();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
exitArmedAt.current = now();
|
|
811
|
+
if (stateRef.current.streaming) abortRef.current?.abort();
|
|
812
|
+
else if (editor.chars.length > 0) setEditor(emptyEditor);
|
|
813
|
+
setNotice("Press Ctrl+C again to exit");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
exitArmedAt.current = void 0;
|
|
817
|
+
if (key.escape) {
|
|
818
|
+
if (stateRef.current.streaming) abortRef.current?.abort();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (input === "\n" && !key.return || key.ctrl && input === "j") {
|
|
822
|
+
setEditor((e) => insert(e, "\n"));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (key.return) {
|
|
826
|
+
if (key.shift || key.meta) setEditor((e) => insert(e, "\n"));
|
|
827
|
+
else if (continuesLine(editor)) setEditor((e) => newlineAfterBackslash(e));
|
|
828
|
+
else submit();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (key.tab) {
|
|
832
|
+
const completed = completeSlash(textOf(editor));
|
|
833
|
+
if (completed) setEditor(fromText(completed));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (key.upArrow) {
|
|
837
|
+
const moved = up(editor);
|
|
838
|
+
if (moved) setEditor(moved);
|
|
839
|
+
else recall(-1);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (key.downArrow) {
|
|
843
|
+
const moved = down(editor);
|
|
844
|
+
if (moved) setEditor(moved);
|
|
845
|
+
else if (historyIndex !== void 0) recall(1);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (key.leftArrow) return setEditor(left);
|
|
849
|
+
if (key.rightArrow) return setEditor(right);
|
|
850
|
+
if (key.home || key.ctrl && input === "a") return setEditor(lineStart);
|
|
851
|
+
if (key.end || key.ctrl && input === "e") return setEditor(lineEnd);
|
|
852
|
+
if (key.backspace) return setEditor(backspace);
|
|
853
|
+
if (key.delete) return setEditor(deleteForward);
|
|
854
|
+
if (key.ctrl && input === "u") return setEditor(killToLineStart);
|
|
855
|
+
if (key.ctrl && input === "k") return setEditor(killToLineEnd);
|
|
856
|
+
if (key.ctrl && input === "w") return setEditor(deleteWordBack);
|
|
857
|
+
if (key.ctrl && input === "l") {
|
|
858
|
+
write("\x1B[2J\x1B[H");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (key.ctrl || key.meta || !input) return;
|
|
862
|
+
setEditor((e) => insert(e, input));
|
|
863
|
+
});
|
|
864
|
+
usePaste((text) => {
|
|
865
|
+
setEditor((e) => insert(e, text));
|
|
866
|
+
});
|
|
867
|
+
const suggestions = slashSuggestions(textOf(editor));
|
|
868
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
|
|
869
|
+
/* @__PURE__ */ jsx4(Static, { items: state.entries, children: (entry) => /* @__PURE__ */ jsx4(EntryView, { entry }, entry.id) }, state.epoch),
|
|
870
|
+
state.streaming ? /* @__PURE__ */ jsx4(StreamingView, { streaming: state.streaming, now: clock }) : null,
|
|
871
|
+
/* @__PURE__ */ jsx4(InputBox, { editor, width, placeholder: PLACEHOLDER }),
|
|
872
|
+
/* @__PURE__ */ jsx4(SlashHints, { commands: suggestions, width }),
|
|
873
|
+
/* @__PURE__ */ jsx4(
|
|
874
|
+
StatusLine,
|
|
875
|
+
{
|
|
876
|
+
state,
|
|
877
|
+
width,
|
|
878
|
+
info: {
|
|
879
|
+
where: backend.inProcess ? `in-process ${new URL(backend.url).port}` : backend.url,
|
|
880
|
+
inProcess: backend.inProcess,
|
|
881
|
+
...notice ? { notice } : {}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
] });
|
|
886
|
+
}
|
|
887
|
+
function ChatApp(props) {
|
|
888
|
+
return /* @__PURE__ */ jsx4(ThemeProvider, { theme: props.theme, children: /* @__PURE__ */ jsx4(Chat, { ...props }) });
|
|
889
|
+
}
|
|
890
|
+
export {
|
|
891
|
+
ChatApp
|
|
892
|
+
};
|
|
893
|
+
//# sourceMappingURL=ChatApp-FO5SABXB.js.map
|