@dmrlzz/davecode 0.0.0-stage → 0.4.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 +34 -2
- package/bin/davecode.js +2 -0
- package/dist/ChatApp-BO7QMBWD.js +1045 -0
- package/dist/ChatApp-BO7QMBWD.js.map +1 -0
- package/dist/RunnerView-A7JSPUW5.js +18 -0
- package/dist/RunnerView-A7JSPUW5.js.map +1 -0
- package/dist/account-UGNLJPQ4.js +23 -0
- package/dist/account-UGNLJPQ4.js.map +1 -0
- package/dist/accounts-NM7IWP53.js +37 -0
- package/dist/accounts-NM7IWP53.js.map +1 -0
- package/dist/budget-INAW6FXD.js +115 -0
- package/dist/budget-INAW6FXD.js.map +1 -0
- package/dist/chat-U4QJQ7M6.js +359 -0
- package/dist/chat-U4QJQ7M6.js.map +1 -0
- package/dist/chunk-3Q2KCXLN.js +9608 -0
- package/dist/chunk-3Q2KCXLN.js.map +1 -0
- package/dist/chunk-5LKOJBUL.js +20 -0
- package/dist/chunk-5LKOJBUL.js.map +1 -0
- package/dist/chunk-7XVXEO7U.js +23 -0
- package/dist/chunk-7XVXEO7U.js.map +1 -0
- package/dist/chunk-CWBVNS3L.js +731 -0
- package/dist/chunk-CWBVNS3L.js.map +1 -0
- package/dist/chunk-DZ2FDABF.js +1614 -0
- package/dist/chunk-DZ2FDABF.js.map +1 -0
- package/dist/chunk-FRFSJMN4.js +79 -0
- package/dist/chunk-FRFSJMN4.js.map +1 -0
- package/dist/chunk-GE22VBCY.js +200 -0
- package/dist/chunk-GE22VBCY.js.map +1 -0
- package/dist/chunk-GHDOUII5.js +144 -0
- package/dist/chunk-GHDOUII5.js.map +1 -0
- package/dist/chunk-GZJ6JSO7.js +213 -0
- package/dist/chunk-GZJ6JSO7.js.map +1 -0
- package/dist/chunk-IC3TDONV.js +12 -0
- package/dist/chunk-IC3TDONV.js.map +1 -0
- package/dist/chunk-MC7RIPL7.js +116 -0
- package/dist/chunk-MC7RIPL7.js.map +1 -0
- package/dist/chunk-MOK5D3XJ.js +114 -0
- package/dist/chunk-MOK5D3XJ.js.map +1 -0
- package/dist/chunk-PGPXE6LF.js +20 -0
- package/dist/chunk-PGPXE6LF.js.map +1 -0
- package/dist/chunk-QA3O6RA2.js +81 -0
- package/dist/chunk-QA3O6RA2.js.map +1 -0
- package/dist/chunk-ROIUTNQV.js +20 -0
- package/dist/chunk-ROIUTNQV.js.map +1 -0
- package/dist/chunk-SF5ZEAP4.js +105 -0
- package/dist/chunk-SF5ZEAP4.js.map +1 -0
- package/dist/chunk-Z6NQDJEN.js +180 -0
- package/dist/chunk-Z6NQDJEN.js.map +1 -0
- package/dist/config-YEAPBMBX.js +158 -0
- package/dist/config-YEAPBMBX.js.map +1 -0
- package/dist/dashboard/assets/Accounts-CeVyX4a3.js +1 -0
- package/dist/dashboard/assets/Badge-CGY6DgGx.js +55 -0
- package/dist/dashboard/assets/Callout-DauCk_cW.js +1 -0
- package/dist/dashboard/assets/Field-OXMnT9Tb.js +1 -0
- package/dist/dashboard/assets/Goals-DH-KDh8M.js +1 -0
- package/dist/dashboard/assets/Library-C15szLjZ.js +1 -0
- package/dist/dashboard/assets/Markdown-Xhao3sgL.js +69 -0
- package/dist/dashboard/assets/Panel-Du40pM7n.js +1 -0
- package/dist/dashboard/assets/RoutesPage-Cj1OelGO.js +4 -0
- package/dist/dashboard/assets/Runner-BK1-ZF7h.js +1 -0
- package/dist/dashboard/assets/Settings-C1nzQQhe.js +1 -0
- package/dist/dashboard/assets/Switch-bwtIJ00H.js +1 -0
- package/dist/dashboard/assets/Tasks-x4s8IiPG.js +2 -0
- package/dist/dashboard/assets/Traffic-CV7TaTid.js +1 -0
- package/dist/dashboard/assets/Workspace-DAL-MBc0.js +1 -0
- package/dist/dashboard/assets/arrow-up-goM9r-I9.js +1 -0
- package/dist/dashboard/assets/forms-DtpfI65-.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-BCi-Vuvh.js +104 -0
- package/dist/dashboard/assets/index-CcJFhc98.css +2 -0
- package/dist/dashboard/assets/task-errors-B2pgjUGN.js +1 -0
- package/dist/dashboard/assets/trash-BVojETtF.js +1 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/github.svg +1 -0
- package/dist/dashboard/index.html +31 -0
- package/dist/dist-RRQGUBL3.js +379 -0
- package/dist/dist-RRQGUBL3.js.map +1 -0
- package/dist/doctor-MQYK44S2.js +279 -0
- package/dist/doctor-MQYK44S2.js.map +1 -0
- package/dist/goal-6NUX4AIT.js +302 -0
- package/dist/goal-6NUX4AIT.js.map +1 -0
- package/dist/index.js +399 -0
- package/dist/index.js.map +1 -0
- package/dist/init-AXYIWAB3.js +194 -0
- package/dist/init-AXYIWAB3.js.map +1 -0
- package/dist/prompts-TZ7UC42M.js +219 -0
- package/dist/prompts-TZ7UC42M.js.map +1 -0
- package/dist/run-QRTLHFB3.js +376 -0
- package/dist/run-QRTLHFB3.js.map +1 -0
- package/dist/runner-state-QFU3LJVJ.js +17 -0
- package/dist/runner-state-QFU3LJVJ.js.map +1 -0
- package/dist/runtime.js +18 -0
- package/dist/runtime.js.map +1 -0
- package/dist/start-O5I5TSO2.js +285 -0
- package/dist/start-O5I5TSO2.js.map +1 -0
- package/dist/status-LEA3RVUX.js +25 -0
- package/dist/status-LEA3RVUX.js.map +1 -0
- package/dist/tasks-XOTVICVZ.js +184 -0
- package/dist/tasks-XOTVICVZ.js.map +1 -0
- package/package.json +73 -3
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatTaskTree,
|
|
3
|
+
summaryLine
|
|
4
|
+
} from "./chunk-MOK5D3XJ.js";
|
|
5
|
+
import {
|
|
6
|
+
brandBanner
|
|
7
|
+
} from "./chunk-ROIUTNQV.js";
|
|
8
|
+
import {
|
|
9
|
+
Spinner
|
|
10
|
+
} from "./chunk-5LKOJBUL.js";
|
|
11
|
+
import {
|
|
12
|
+
ThemeProvider,
|
|
13
|
+
useTheme
|
|
14
|
+
} from "./chunk-PGPXE6LF.js";
|
|
15
|
+
import {
|
|
16
|
+
emptyUsage,
|
|
17
|
+
renderAccounts
|
|
18
|
+
} from "./chunk-GZJ6JSO7.js";
|
|
19
|
+
import {
|
|
20
|
+
bar,
|
|
21
|
+
formatTokens,
|
|
22
|
+
levelStyle,
|
|
23
|
+
padEnd,
|
|
24
|
+
percent,
|
|
25
|
+
visibleWidth
|
|
26
|
+
} from "./chunk-SF5ZEAP4.js";
|
|
27
|
+
import "./chunk-QA3O6RA2.js";
|
|
28
|
+
import "./chunk-DZ2FDABF.js";
|
|
29
|
+
import "./chunk-7XVXEO7U.js";
|
|
30
|
+
import "./chunk-3Q2KCXLN.js";
|
|
31
|
+
import "./chunk-MC7RIPL7.js";
|
|
32
|
+
|
|
33
|
+
// src/tui/ChatApp.tsx
|
|
34
|
+
import { Box as Box5, Static, useApp, useInput, usePaste, useStdout, useWindowSize } from "ink";
|
|
35
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
36
|
+
|
|
37
|
+
// src/tui/chat-state.ts
|
|
38
|
+
function initialChatState(model) {
|
|
39
|
+
return {
|
|
40
|
+
entries: [],
|
|
41
|
+
conversation: [],
|
|
42
|
+
model,
|
|
43
|
+
connection: "connecting",
|
|
44
|
+
lastFailovers: 0,
|
|
45
|
+
totalFailovers: 0,
|
|
46
|
+
tokens: { prompt: 0, completion: 0 },
|
|
47
|
+
epoch: 0,
|
|
48
|
+
history: [],
|
|
49
|
+
nextId: 1
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function estimateTokens(text) {
|
|
53
|
+
return Math.ceil(text.length / 4);
|
|
54
|
+
}
|
|
55
|
+
function push(state, kind, text, meta) {
|
|
56
|
+
const entry = { id: state.nextId, kind, text };
|
|
57
|
+
if (meta) entry.meta = meta;
|
|
58
|
+
return { ...state, entries: [...state.entries, entry], nextId: state.nextId + 1 };
|
|
59
|
+
}
|
|
60
|
+
function chatReducer(state, action) {
|
|
61
|
+
switch (action.type) {
|
|
62
|
+
case "banner":
|
|
63
|
+
return push(state, "banner", action.text);
|
|
64
|
+
case "info":
|
|
65
|
+
return push(state, "info", action.text);
|
|
66
|
+
case "error":
|
|
67
|
+
return push(state, "error", action.text);
|
|
68
|
+
case "user": {
|
|
69
|
+
const next = push(state, "user", action.text);
|
|
70
|
+
const history = state.history[state.history.length - 1] === action.text ? state.history : [...state.history, action.text].slice(-200);
|
|
71
|
+
return {
|
|
72
|
+
...next,
|
|
73
|
+
history,
|
|
74
|
+
conversation: [...state.conversation, { role: "user", content: action.text }]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "stream-start":
|
|
78
|
+
return {
|
|
79
|
+
...state,
|
|
80
|
+
streaming: { id: state.nextId, text: "", startedAt: action.now, model: state.model },
|
|
81
|
+
nextId: state.nextId + 1
|
|
82
|
+
};
|
|
83
|
+
case "stream-meta": {
|
|
84
|
+
if (!state.streaming) return state;
|
|
85
|
+
const streaming = { ...state.streaming };
|
|
86
|
+
if (action.accountId) streaming.accountId = action.accountId;
|
|
87
|
+
if (action.provider) streaming.provider = action.provider;
|
|
88
|
+
if (action.failovers !== void 0) streaming.failovers = action.failovers;
|
|
89
|
+
const lastAccount = action.accountId ? {
|
|
90
|
+
id: action.accountId,
|
|
91
|
+
...action.accountLabel ? { label: action.accountLabel } : {},
|
|
92
|
+
...action.provider ? { provider: action.provider } : {}
|
|
93
|
+
} : state.lastAccount;
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
streaming,
|
|
97
|
+
...lastAccount ? { lastAccount } : {},
|
|
98
|
+
...action.failovers !== void 0 ? {
|
|
99
|
+
lastFailovers: action.failovers,
|
|
100
|
+
totalFailovers: state.totalFailovers + action.failovers
|
|
101
|
+
} : {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case "stream-delta":
|
|
105
|
+
if (!state.streaming) return state;
|
|
106
|
+
return {
|
|
107
|
+
...state,
|
|
108
|
+
streaming: { ...state.streaming, text: state.streaming.text + action.text }
|
|
109
|
+
};
|
|
110
|
+
case "stream-end": {
|
|
111
|
+
const live = state.streaming;
|
|
112
|
+
if (!live) return state;
|
|
113
|
+
const prompt = action.usage?.prompt_tokens ?? estimateTokens(
|
|
114
|
+
state.conversation.map((m) => typeof m.content === "string" ? m.content : "").join("\n")
|
|
115
|
+
);
|
|
116
|
+
const completion = action.usage?.completion_tokens ?? estimateTokens(live.text);
|
|
117
|
+
const meta = {
|
|
118
|
+
model: live.model,
|
|
119
|
+
latencyMs: action.now - live.startedAt,
|
|
120
|
+
tokens: prompt + completion,
|
|
121
|
+
...action.usage ? {} : { estimated: true },
|
|
122
|
+
...live.accountId ? { accountId: live.accountId } : {},
|
|
123
|
+
...state.lastAccount?.id === live.accountId && state.lastAccount?.label ? { accountLabel: state.lastAccount.label } : {},
|
|
124
|
+
...live.provider ? { provider: live.provider } : {},
|
|
125
|
+
...live.failovers ? { failovers: live.failovers } : {},
|
|
126
|
+
...action.cancelled ? { cancelled: true } : {}
|
|
127
|
+
};
|
|
128
|
+
let next = { ...state };
|
|
129
|
+
delete next.streaming;
|
|
130
|
+
const answered = live.text.length > 0;
|
|
131
|
+
if (answered || action.cancelled) next = push(next, "assistant", live.text, meta);
|
|
132
|
+
if (action.error) next = push(next, "error", action.error);
|
|
133
|
+
if (answered && !action.cancelled && !action.error) {
|
|
134
|
+
next.conversation = [...next.conversation, { role: "assistant", content: live.text }];
|
|
135
|
+
} else if (next.conversation[next.conversation.length - 1]?.role === "user") {
|
|
136
|
+
next.conversation = next.conversation.slice(0, -1);
|
|
137
|
+
}
|
|
138
|
+
if (answered || action.usage) {
|
|
139
|
+
next.tokens = {
|
|
140
|
+
prompt: state.tokens.prompt + prompt,
|
|
141
|
+
completion: state.tokens.completion + completion
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
case "set-model":
|
|
147
|
+
return { ...state, model: action.model };
|
|
148
|
+
case "connection":
|
|
149
|
+
return state.connection === action.connection ? state : { ...state, connection: action.connection };
|
|
150
|
+
case "quota": {
|
|
151
|
+
if (state.lastAccount?.id !== action.accountId) return state;
|
|
152
|
+
const next = { ...state };
|
|
153
|
+
if (action.utilization === void 0) delete next.quota5h;
|
|
154
|
+
else next.quota5h = action.utilization;
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
case "clear": {
|
|
158
|
+
const next = {
|
|
159
|
+
...state,
|
|
160
|
+
entries: [],
|
|
161
|
+
conversation: [],
|
|
162
|
+
epoch: state.epoch + 1
|
|
163
|
+
};
|
|
164
|
+
delete next.streaming;
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
return state;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/tui/components/InputBox.tsx
|
|
173
|
+
import { Box, Text } from "ink";
|
|
174
|
+
|
|
175
|
+
// src/tui/editor.ts
|
|
176
|
+
var emptyEditor = { chars: [], cursor: 0 };
|
|
177
|
+
function fromText(text) {
|
|
178
|
+
const chars = [...text];
|
|
179
|
+
return { chars, cursor: chars.length };
|
|
180
|
+
}
|
|
181
|
+
function textOf(state) {
|
|
182
|
+
return state.chars.join("");
|
|
183
|
+
}
|
|
184
|
+
function normalizeInput(text) {
|
|
185
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, "");
|
|
186
|
+
}
|
|
187
|
+
function insert(state, text) {
|
|
188
|
+
const add = [...normalizeInput(text)];
|
|
189
|
+
if (add.length === 0) return state;
|
|
190
|
+
const chars = [...state.chars.slice(0, state.cursor), ...add, ...state.chars.slice(state.cursor)];
|
|
191
|
+
return { chars, cursor: state.cursor + add.length };
|
|
192
|
+
}
|
|
193
|
+
function backspace(state) {
|
|
194
|
+
if (state.cursor === 0) return state;
|
|
195
|
+
const chars = [...state.chars.slice(0, state.cursor - 1), ...state.chars.slice(state.cursor)];
|
|
196
|
+
return { chars, cursor: state.cursor - 1 };
|
|
197
|
+
}
|
|
198
|
+
function deleteForward(state) {
|
|
199
|
+
if (state.cursor >= state.chars.length) return state;
|
|
200
|
+
const chars = [...state.chars.slice(0, state.cursor), ...state.chars.slice(state.cursor + 1)];
|
|
201
|
+
return { chars, cursor: state.cursor };
|
|
202
|
+
}
|
|
203
|
+
function left(state) {
|
|
204
|
+
return { ...state, cursor: Math.max(0, state.cursor - 1) };
|
|
205
|
+
}
|
|
206
|
+
function right(state) {
|
|
207
|
+
return { ...state, cursor: Math.min(state.chars.length, state.cursor + 1) };
|
|
208
|
+
}
|
|
209
|
+
function position(state) {
|
|
210
|
+
let line = 0;
|
|
211
|
+
let column = 0;
|
|
212
|
+
for (let i = 0; i < state.cursor; i++) {
|
|
213
|
+
if (state.chars[i] === "\n") {
|
|
214
|
+
line++;
|
|
215
|
+
column = 0;
|
|
216
|
+
} else {
|
|
217
|
+
column++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { line, column };
|
|
221
|
+
}
|
|
222
|
+
function lines(state) {
|
|
223
|
+
const out = [[]];
|
|
224
|
+
for (const ch of state.chars) {
|
|
225
|
+
if (ch === "\n") out.push([]);
|
|
226
|
+
else out[out.length - 1].push(ch);
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
function offsetOf(all, line, column) {
|
|
231
|
+
let offset = 0;
|
|
232
|
+
for (let i = 0; i < line; i++) offset += all[i].length + 1;
|
|
233
|
+
return offset + Math.min(column, all[line].length);
|
|
234
|
+
}
|
|
235
|
+
function up(state) {
|
|
236
|
+
const { line, column } = position(state);
|
|
237
|
+
if (line === 0) return void 0;
|
|
238
|
+
return { ...state, cursor: offsetOf(lines(state), line - 1, column) };
|
|
239
|
+
}
|
|
240
|
+
function down(state) {
|
|
241
|
+
const all = lines(state);
|
|
242
|
+
const { line, column } = position(state);
|
|
243
|
+
if (line >= all.length - 1) return void 0;
|
|
244
|
+
return { ...state, cursor: offsetOf(all, line + 1, column) };
|
|
245
|
+
}
|
|
246
|
+
function lineStart(state) {
|
|
247
|
+
const { line } = position(state);
|
|
248
|
+
return { ...state, cursor: offsetOf(lines(state), line, 0) };
|
|
249
|
+
}
|
|
250
|
+
function lineEnd(state) {
|
|
251
|
+
const all = lines(state);
|
|
252
|
+
const { line } = position(state);
|
|
253
|
+
return { ...state, cursor: offsetOf(all, line, all[line].length) };
|
|
254
|
+
}
|
|
255
|
+
function killToLineStart(state) {
|
|
256
|
+
const start = lineStart(state).cursor;
|
|
257
|
+
if (start === state.cursor) return state;
|
|
258
|
+
return {
|
|
259
|
+
chars: [...state.chars.slice(0, start), ...state.chars.slice(state.cursor)],
|
|
260
|
+
cursor: start
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function killToLineEnd(state) {
|
|
264
|
+
const end = lineEnd(state).cursor;
|
|
265
|
+
return {
|
|
266
|
+
chars: [...state.chars.slice(0, state.cursor), ...state.chars.slice(end)],
|
|
267
|
+
cursor: state.cursor
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function deleteWordBack(state) {
|
|
271
|
+
let i = state.cursor;
|
|
272
|
+
while (i > 0 && /[ \t]/.test(state.chars[i - 1])) i--;
|
|
273
|
+
while (i > 0 && !/[\s]/.test(state.chars[i - 1])) i--;
|
|
274
|
+
if (i === state.cursor) return backspace(state);
|
|
275
|
+
return { chars: [...state.chars.slice(0, i), ...state.chars.slice(state.cursor)], cursor: i };
|
|
276
|
+
}
|
|
277
|
+
function continuesLine(state) {
|
|
278
|
+
return state.cursor > 0 && state.chars[state.cursor - 1] === "\\";
|
|
279
|
+
}
|
|
280
|
+
function newlineAfterBackslash(state) {
|
|
281
|
+
return insert(backspace(state), "\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/tui/components/InputBox.tsx
|
|
285
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
286
|
+
function cursorParts(line, column) {
|
|
287
|
+
if (column === void 0) return { before: line.join(""), at: "", after: "" };
|
|
288
|
+
return {
|
|
289
|
+
before: line.slice(0, column).join(""),
|
|
290
|
+
at: line[column] ?? " ",
|
|
291
|
+
after: line.slice(column + 1).join("")
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function InputBox({
|
|
295
|
+
editor,
|
|
296
|
+
width,
|
|
297
|
+
placeholder,
|
|
298
|
+
disabled
|
|
299
|
+
}) {
|
|
300
|
+
const theme = useTheme();
|
|
301
|
+
const all = lines(editor);
|
|
302
|
+
const cursor = position(editor);
|
|
303
|
+
const empty = editor.chars.length === 0;
|
|
304
|
+
return /* @__PURE__ */ jsx(
|
|
305
|
+
Box,
|
|
306
|
+
{
|
|
307
|
+
borderStyle: theme.unicode ? "round" : "classic",
|
|
308
|
+
borderDimColor: theme.ink.dim,
|
|
309
|
+
flexDirection: "column",
|
|
310
|
+
paddingX: 1,
|
|
311
|
+
width,
|
|
312
|
+
children: all.map((line, i) => {
|
|
313
|
+
const parts = cursorParts(line, !disabled && i === cursor.line ? cursor.column : void 0);
|
|
314
|
+
return (
|
|
315
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: lines have no identity beyond position
|
|
316
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
317
|
+
/* @__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 : "" }) }),
|
|
318
|
+
empty && i === 0 ? /* @__PURE__ */ jsxs(Text, { children: [
|
|
319
|
+
disabled ? null : /* @__PURE__ */ jsx(Text, { inverse: true, children: " " }),
|
|
320
|
+
/* @__PURE__ */ jsx(Text, { dimColor: theme.ink.dim, children: placeholder })
|
|
321
|
+
] }) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
322
|
+
parts.before,
|
|
323
|
+
parts.at ? /* @__PURE__ */ jsx(Text, { inverse: true, children: parts.at }) : null,
|
|
324
|
+
parts.after
|
|
325
|
+
] })
|
|
326
|
+
] }, i)
|
|
327
|
+
);
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function SlashHints({
|
|
333
|
+
commands,
|
|
334
|
+
width,
|
|
335
|
+
selected = 0
|
|
336
|
+
}) {
|
|
337
|
+
const theme = useTheme();
|
|
338
|
+
if (commands.length === 0) return null;
|
|
339
|
+
const nameWidth = Math.max(...commands.map((c) => c.name.length + (c.args ? c.args.length + 1 : 0))) + 3;
|
|
340
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingX: 1, width, children: commands.map((c, i) => {
|
|
341
|
+
const active = i === selected;
|
|
342
|
+
return /* @__PURE__ */ jsxs(Text, { wrap: "truncate-end", children: [
|
|
343
|
+
/* @__PURE__ */ jsx(Text, { bold: active, dimColor: !active && theme.ink.dim, children: `${active ? theme.glyph.prompt : " "} ${`/${c.name}${c.args ? ` ${c.args}` : ""}`.padEnd(nameWidth)}` }),
|
|
344
|
+
/* @__PURE__ */ jsx(Text, { dimColor: theme.ink.dim, children: c.description })
|
|
345
|
+
] }, c.name);
|
|
346
|
+
}) });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/tui/components/Picker.tsx
|
|
350
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
351
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
352
|
+
function Picker({
|
|
353
|
+
title,
|
|
354
|
+
options,
|
|
355
|
+
index
|
|
356
|
+
}) {
|
|
357
|
+
const theme = useTheme();
|
|
358
|
+
const numbered = options.length <= 9;
|
|
359
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
360
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, children: title }),
|
|
361
|
+
options.map((option, i) => {
|
|
362
|
+
const on = i === index;
|
|
363
|
+
return /* @__PURE__ */ jsxs2(Text2, { dimColor: !on && theme.ink.dim, children: [
|
|
364
|
+
on ? "\u203A " : " ",
|
|
365
|
+
numbered ? `${i + 1}. ` : "",
|
|
366
|
+
/* @__PURE__ */ jsx2(Text2, { bold: on, children: option.label }),
|
|
367
|
+
option.hint ? /* @__PURE__ */ jsxs2(Text2, { dimColor: theme.ink.dim, children: [
|
|
368
|
+
" ",
|
|
369
|
+
option.hint
|
|
370
|
+
] }) : null
|
|
371
|
+
] }, option.value);
|
|
372
|
+
}),
|
|
373
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: theme.ink.dim, children: [
|
|
374
|
+
"\u2191\u2193 select \xB7 enter choose",
|
|
375
|
+
numbered ? " \xB7 1-9" : "",
|
|
376
|
+
" \xB7 esc cancel"
|
|
377
|
+
] })
|
|
378
|
+
] });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/tui/components/StatusLine.tsx
|
|
382
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
383
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
384
|
+
function statusSegments(theme, state, info, width) {
|
|
385
|
+
const sep = theme.dim(" \xB7 ");
|
|
386
|
+
const left2 = [[9, theme.accent(state.model)]];
|
|
387
|
+
if (state.lastAccount) {
|
|
388
|
+
const name = state.lastAccount.label ?? state.lastAccount.id;
|
|
389
|
+
left2.push([
|
|
390
|
+
8,
|
|
391
|
+
`${name}${state.lastAccount.provider ? theme.dim(` (${state.lastAccount.provider})`) : ""}`
|
|
392
|
+
]);
|
|
393
|
+
}
|
|
394
|
+
if (state.totalFailovers > 0) {
|
|
395
|
+
left2.push([7, theme.warn(`${state.lastFailovers}/${state.totalFailovers} failovers`)]);
|
|
396
|
+
}
|
|
397
|
+
if (state.tokens.prompt + state.tokens.completion > 0) {
|
|
398
|
+
const arrows = theme.unicode ? ["\u2191", "\u2193"] : ["in", "out"];
|
|
399
|
+
left2.push([
|
|
400
|
+
5,
|
|
401
|
+
theme.dim(
|
|
402
|
+
`${formatTokens(state.tokens.prompt)}${arrows[0]} ${formatTokens(state.tokens.completion)}${arrows[1]}`
|
|
403
|
+
)
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
if (state.quota5h !== void 0) {
|
|
407
|
+
left2.push([
|
|
408
|
+
6,
|
|
409
|
+
`${theme.dim("5h")} ${bar(theme, state.quota5h, 6)} ${levelStyle(theme, state.quota5h)(percent(state.quota5h))}`
|
|
410
|
+
]);
|
|
411
|
+
}
|
|
412
|
+
let right2 = "";
|
|
413
|
+
if (info.notice) right2 = theme.warn(info.notice);
|
|
414
|
+
else if (state.connection === "connecting") right2 = theme.dim("connecting");
|
|
415
|
+
else if (state.connection === "offline") right2 = theme.error("offline");
|
|
416
|
+
let kept = left2;
|
|
417
|
+
const render = () => kept.map(([, text]) => text).join(sep);
|
|
418
|
+
while (kept.length > 1 && visibleWidth(render()) + visibleWidth(right2) + 2 > width) {
|
|
419
|
+
const lowest = Math.min(...kept.map(([p]) => p));
|
|
420
|
+
kept = kept.filter(([p]) => p !== lowest);
|
|
421
|
+
}
|
|
422
|
+
return { left: render(), right: right2 };
|
|
423
|
+
}
|
|
424
|
+
function StatusLine({
|
|
425
|
+
state,
|
|
426
|
+
info,
|
|
427
|
+
width
|
|
428
|
+
}) {
|
|
429
|
+
const theme = useTheme();
|
|
430
|
+
const { left: left2, right: right2 } = statusSegments(theme, state, info, width - 2);
|
|
431
|
+
return /* @__PURE__ */ jsxs3(Box3, { width, justifyContent: "space-between", paddingX: 1, children: [
|
|
432
|
+
/* @__PURE__ */ jsx3(Text3, { wrap: "truncate-end", children: left2 }),
|
|
433
|
+
/* @__PURE__ */ jsx3(Text3, { wrap: "truncate-start", children: right2 })
|
|
434
|
+
] });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/tui/components/Transcript.tsx
|
|
438
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
439
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
440
|
+
function metaLine(meta) {
|
|
441
|
+
const parts = [];
|
|
442
|
+
if (meta.accountLabel ?? meta.accountId) {
|
|
443
|
+
parts.push([meta.provider, meta.accountLabel ?? meta.accountId].filter(Boolean).join(" \xB7 "));
|
|
444
|
+
}
|
|
445
|
+
if (meta.model) parts.push(meta.model);
|
|
446
|
+
if (meta.tokens !== void 0)
|
|
447
|
+
parts.push(`${meta.estimated ? "~" : ""}${formatTokens(meta.tokens)} tok`);
|
|
448
|
+
if (meta.latencyMs !== void 0) parts.push(`${(meta.latencyMs / 1e3).toFixed(1)}s`);
|
|
449
|
+
if (meta.failovers) parts.push(`${meta.failovers} failover${meta.failovers === 1 ? "" : "s"}`);
|
|
450
|
+
if (meta.cancelled) parts.push("cancelled");
|
|
451
|
+
return parts.join(" \xB7 ");
|
|
452
|
+
}
|
|
453
|
+
function Marker({ color, glyph }) {
|
|
454
|
+
return /* @__PURE__ */ jsx4(Box4, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { color, children: glyph }) });
|
|
455
|
+
}
|
|
456
|
+
function EntryView({ entry }) {
|
|
457
|
+
const theme = useTheme();
|
|
458
|
+
switch (entry.kind) {
|
|
459
|
+
case "banner":
|
|
460
|
+
return /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { children: entry.text }) });
|
|
461
|
+
case "user":
|
|
462
|
+
return /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
|
|
463
|
+
/* @__PURE__ */ jsx4(Marker, { color: theme.ink.accent, glyph: theme.glyph.prompt }),
|
|
464
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: theme.ink.dim, children: entry.text })
|
|
465
|
+
] });
|
|
466
|
+
case "assistant":
|
|
467
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
468
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
469
|
+
/* @__PURE__ */ jsx4(Marker, { color: theme.ink.accent, glyph: theme.glyph.dot }),
|
|
470
|
+
/* @__PURE__ */ jsx4(Text4, { children: entry.text || (entry.meta?.cancelled ? "(cancelled before any output)" : "") })
|
|
471
|
+
] }),
|
|
472
|
+
entry.meta ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: theme.ink.dim, children: metaLine(entry.meta) }) }) : null
|
|
473
|
+
] });
|
|
474
|
+
case "error":
|
|
475
|
+
return /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
|
|
476
|
+
/* @__PURE__ */ jsx4(Marker, { color: theme.ink.error, glyph: theme.glyph.fail }),
|
|
477
|
+
/* @__PURE__ */ jsx4(Text4, { color: theme.ink.error, children: entry.text })
|
|
478
|
+
] });
|
|
479
|
+
default:
|
|
480
|
+
return /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { children: entry.text }) });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function StreamingView({ streaming, now }) {
|
|
484
|
+
const theme = useTheme();
|
|
485
|
+
const seconds = Math.max(0, (now - streaming.startedAt) / 1e3).toFixed(0);
|
|
486
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
487
|
+
streaming.text ? /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
488
|
+
/* @__PURE__ */ jsx4(Marker, { color: theme.ink.accent, glyph: theme.glyph.dot }),
|
|
489
|
+
/* @__PURE__ */ jsx4(Text4, { children: streaming.text })
|
|
490
|
+
] }) : null,
|
|
491
|
+
/* @__PURE__ */ jsxs4(Box4, { paddingLeft: streaming.text ? 2 : 0, children: [
|
|
492
|
+
/* @__PURE__ */ jsx4(Spinner, {}),
|
|
493
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: theme.ink.dim, children: [
|
|
494
|
+
" ",
|
|
495
|
+
streaming.text ? "streaming" : "thinking",
|
|
496
|
+
" ",
|
|
497
|
+
seconds,
|
|
498
|
+
"s \xB7 esc to cancel"
|
|
499
|
+
] })
|
|
500
|
+
] })
|
|
501
|
+
] });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/tui/slash.ts
|
|
505
|
+
var SLASH_COMMANDS = [
|
|
506
|
+
{ name: "model", args: "[id]", description: "show models or switch, e.g. /model davecode/auto" },
|
|
507
|
+
{
|
|
508
|
+
name: "connect",
|
|
509
|
+
description: "connect Claude Code, Codex or Antigravity, or sign in to one again"
|
|
510
|
+
},
|
|
511
|
+
{ name: "route", description: "routes and their failover targets" },
|
|
512
|
+
{ name: "accounts", description: "accounts with 5h quota usage" },
|
|
513
|
+
{ name: "tasks", description: "the project task graph" },
|
|
514
|
+
{ name: "status", description: "model, agents and whether they are signed in" },
|
|
515
|
+
{ name: "login", description: "sign in to your DaveCode account in the browser" },
|
|
516
|
+
{ name: "logout", description: "sign out of your DaveCode account" },
|
|
517
|
+
{ name: "account", description: "the signed-in DaveCode account" },
|
|
518
|
+
{ name: "clear", description: "clear the transcript and start a new conversation" },
|
|
519
|
+
{ name: "help", description: "commands and keyboard shortcuts", aliases: ["?"] },
|
|
520
|
+
{ name: "exit", description: "quit DaveCode", aliases: ["quit", "q"] }
|
|
521
|
+
];
|
|
522
|
+
function parseSlash(input) {
|
|
523
|
+
const text = input.trim();
|
|
524
|
+
if (!text.startsWith("/") || text.startsWith("//")) return { kind: "none" };
|
|
525
|
+
const [head = "", ...rest] = text.slice(1).split(/\s+/);
|
|
526
|
+
const name = head.toLowerCase();
|
|
527
|
+
if (!name) return { kind: "unknown", name: "" };
|
|
528
|
+
const command = SLASH_COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
|
|
529
|
+
if (!command) return { kind: "unknown", name };
|
|
530
|
+
return { kind: "command", command, args: rest.join(" ").trim() };
|
|
531
|
+
}
|
|
532
|
+
function slashSuggestions(input) {
|
|
533
|
+
if (!input.startsWith("/") || /\s/.test(input)) return [];
|
|
534
|
+
const prefix = input.slice(1).toLowerCase();
|
|
535
|
+
return SLASH_COMMANDS.filter(
|
|
536
|
+
(c) => c.name.startsWith(prefix) || c.aliases?.some((a) => a.startsWith(prefix) && prefix)
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
var KEY_HINTS = [
|
|
540
|
+
["Enter", "send"],
|
|
541
|
+
["Shift+Enter, Ctrl+J or \\ Enter", "new line"],
|
|
542
|
+
["\u2191 / \u2193", "move between lines, then through history"],
|
|
543
|
+
["/", "command menu: \u2191 / \u2193 to choose, Enter to run, Tab to fill, Esc to close"],
|
|
544
|
+
["Esc", "cancel the response in flight"],
|
|
545
|
+
["Ctrl+U / Ctrl+K / Ctrl+W", "delete to line start / line end / previous word"],
|
|
546
|
+
["Ctrl+C twice", "exit"]
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
// src/tui/slash-actions.ts
|
|
550
|
+
function helpText(theme) {
|
|
551
|
+
const width = Math.max(...SLASH_COMMANDS.map((c) => c.name.length + (c.args?.length ?? 0))) + 4;
|
|
552
|
+
const commands = SLASH_COMMANDS.map(
|
|
553
|
+
(c) => `${theme.accent(padEnd(`/${c.name}${c.args ? ` ${c.args}` : ""}`, width))}${theme.dim(c.description)}`
|
|
554
|
+
);
|
|
555
|
+
const keyWidth = Math.max(...KEY_HINTS.map(([k]) => k.length)) + 2;
|
|
556
|
+
const keys = KEY_HINTS.map(([k, v]) => `${padEnd(k, keyWidth)}${theme.dim(v)}`);
|
|
557
|
+
return [theme.bold("Commands"), ...commands, "", theme.bold("Keys"), ...keys].join("\n");
|
|
558
|
+
}
|
|
559
|
+
async function modelsText(theme, backend, current) {
|
|
560
|
+
const models = await backend.models();
|
|
561
|
+
const ids = models.map((m) => m.id).sort((a, b) => {
|
|
562
|
+
const da = a.startsWith("davecode/") ? 0 : 1;
|
|
563
|
+
const db = b.startsWith("davecode/") ? 0 : 1;
|
|
564
|
+
return da - db || a.localeCompare(b);
|
|
565
|
+
});
|
|
566
|
+
if (ids.length === 0)
|
|
567
|
+
return theme.dim("No models yet. Add an account with `davecode accounts add`.");
|
|
568
|
+
const shown = ids.slice(0, 40);
|
|
569
|
+
const lines2 = shown.map(
|
|
570
|
+
(id) => id === current ? `${theme.accent(theme.glyph.dot)} ${theme.accent(id)}` : ` ${id}`
|
|
571
|
+
);
|
|
572
|
+
if (ids.length > shown.length) lines2.push(theme.dim(` \u2026 and ${ids.length - shown.length} more`));
|
|
573
|
+
lines2.push("", theme.dim("Switch with /model <id>"));
|
|
574
|
+
return lines2.join("\n");
|
|
575
|
+
}
|
|
576
|
+
async function routesText(theme, backend) {
|
|
577
|
+
const { defaultRoute, routes } = await backend.routes();
|
|
578
|
+
if (routes.length === 0) {
|
|
579
|
+
return [
|
|
580
|
+
`${theme.dim("No routes configured.")} davecode/${defaultRoute} uses every account with a default model.`,
|
|
581
|
+
theme.dim("Define routes under routing.routes in ~/.davecode/config.json.")
|
|
582
|
+
].join("\n");
|
|
583
|
+
}
|
|
584
|
+
const lines2 = [];
|
|
585
|
+
for (const route of routes) {
|
|
586
|
+
const mark = route.name === defaultRoute ? theme.dim(" (default)") : "";
|
|
587
|
+
lines2.push(
|
|
588
|
+
`${theme.accent(`davecode/${route.name}`)}${mark}${route.description ? theme.dim(` ${route.description}`) : ""}`
|
|
589
|
+
);
|
|
590
|
+
route.targets.forEach((t, i) => {
|
|
591
|
+
lines2.push(
|
|
592
|
+
` ${theme.dim(`${i + 1}.`)} ${t.provider}/${t.model}${t.accountId ? theme.dim(` @${t.accountId}`) : ""}`
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
return lines2.join("\n");
|
|
597
|
+
}
|
|
598
|
+
async function accountsText(theme, backend, columns) {
|
|
599
|
+
const [accounts, usage] = await Promise.all([backend.accounts(), backend.usage()]);
|
|
600
|
+
if (accounts.length === 0)
|
|
601
|
+
return theme.dim("No accounts yet. Add one with `davecode accounts add`.");
|
|
602
|
+
const byId = new Map(usage.map((u) => [u.accountId, u]));
|
|
603
|
+
return renderAccounts(
|
|
604
|
+
theme,
|
|
605
|
+
accounts.map((account) => ({ account, usage: byId.get(account.id) ?? emptyUsage(account.id) })),
|
|
606
|
+
columns
|
|
607
|
+
).join("\n");
|
|
608
|
+
}
|
|
609
|
+
async function tasksText(theme, backend, columns) {
|
|
610
|
+
const { project, graph } = await backend.tasks();
|
|
611
|
+
if (!project)
|
|
612
|
+
return theme.dim("No project brain is being served. Start DaveCode inside a repository.");
|
|
613
|
+
if (graph.tasks.length === 0) {
|
|
614
|
+
return theme.dim(`${project.name}: no tasks yet. Add one with \`davecode tasks add\`.`);
|
|
615
|
+
}
|
|
616
|
+
return [
|
|
617
|
+
`${theme.bold(project.name)} ${summaryLine(theme, graph)}`,
|
|
618
|
+
"",
|
|
619
|
+
...formatTaskTree(theme, graph, { width: columns })
|
|
620
|
+
].join("\n");
|
|
621
|
+
}
|
|
622
|
+
async function statusText(theme, backend, model) {
|
|
623
|
+
const health = await backend.health();
|
|
624
|
+
if (!health) return theme.error("DaveCode is not answering. Restart it with davecode.");
|
|
625
|
+
const [accounts, runner] = await Promise.all([
|
|
626
|
+
backend.accounts(),
|
|
627
|
+
backend.runner().catch(() => void 0)
|
|
628
|
+
]);
|
|
629
|
+
const state = (status) => status === "error" ? theme.error("needs sign-in \xB7 /connect") : status === "cooldown" ? theme.warn("cooling down") : status === "disabled" ? theme.dim("off") : theme.ok("ready");
|
|
630
|
+
const lines2 = [`${theme.dim("model ")} ${model}`];
|
|
631
|
+
if (accounts.length === 0) lines2.push(`${theme.dim("agents")} none yet \xB7 /connect`);
|
|
632
|
+
for (const [i, account] of accounts.entries()) {
|
|
633
|
+
lines2.push(
|
|
634
|
+
`${theme.dim(i === 0 ? "agents" : " ")} ${account.label} ${state(account.status)}`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
lines2.push(`${theme.dim("runner")} ${runner ? runner.state : "unknown"}`);
|
|
638
|
+
return lines2.join("\n");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/tui/ChatApp.tsx
|
|
642
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
643
|
+
var PLACEHOLDER = "Ask anything, or type / for commands";
|
|
644
|
+
var EXIT_WINDOW_MS = 2e3;
|
|
645
|
+
function errorText(err) {
|
|
646
|
+
return err instanceof Error ? err.message : String(err);
|
|
647
|
+
}
|
|
648
|
+
function Chat({
|
|
649
|
+
backend,
|
|
650
|
+
model,
|
|
651
|
+
version,
|
|
652
|
+
project,
|
|
653
|
+
account,
|
|
654
|
+
connect,
|
|
655
|
+
pollMs = 1e4,
|
|
656
|
+
now = Date.now
|
|
657
|
+
}) {
|
|
658
|
+
const theme = useTheme();
|
|
659
|
+
const { exit } = useApp();
|
|
660
|
+
const { write } = useStdout();
|
|
661
|
+
const { columns } = useWindowSize();
|
|
662
|
+
const width = Math.max(20, columns);
|
|
663
|
+
const widthRef = useRef(width);
|
|
664
|
+
widthRef.current = width;
|
|
665
|
+
const [state, dispatch] = useReducer(chatReducer, model, initialChatState);
|
|
666
|
+
const [editor, setEditor] = useState(emptyEditor);
|
|
667
|
+
const [historyIndex, setHistoryIndex] = useState();
|
|
668
|
+
const [notice, setNotice] = useState();
|
|
669
|
+
const [clock, setClock] = useState(now());
|
|
670
|
+
const [slashIndex, setSlashIndex] = useState(0);
|
|
671
|
+
const [picker, setPicker] = useState();
|
|
672
|
+
const abortRef = useRef(void 0);
|
|
673
|
+
const exitArmedAt = useRef(void 0);
|
|
674
|
+
const labels = useRef(/* @__PURE__ */ new Map());
|
|
675
|
+
const stateRef = useRef(state);
|
|
676
|
+
stateRef.current = state;
|
|
677
|
+
useEffect(() => {
|
|
678
|
+
dispatch({
|
|
679
|
+
type: "banner",
|
|
680
|
+
text: brandBanner(
|
|
681
|
+
theme,
|
|
682
|
+
[
|
|
683
|
+
`${theme.bold("DaveCode")} ${theme.dim(`v${version}`)}`,
|
|
684
|
+
...project ? [theme.dim(project)] : []
|
|
685
|
+
],
|
|
686
|
+
widthRef.current
|
|
687
|
+
).join("\n")
|
|
688
|
+
});
|
|
689
|
+
let alive = true;
|
|
690
|
+
const check = async () => {
|
|
691
|
+
const health = await backend.health();
|
|
692
|
+
if (alive) dispatch({ type: "connection", connection: health ? "online" : "offline" });
|
|
693
|
+
};
|
|
694
|
+
void check();
|
|
695
|
+
backend.accounts().then((accounts) => {
|
|
696
|
+
for (const a of accounts) labels.current.set(a.id, a.label);
|
|
697
|
+
}).catch(() => {
|
|
698
|
+
});
|
|
699
|
+
const timer = setInterval(() => void check(), pollMs);
|
|
700
|
+
return () => {
|
|
701
|
+
alive = false;
|
|
702
|
+
clearInterval(timer);
|
|
703
|
+
abortRef.current?.abort();
|
|
704
|
+
};
|
|
705
|
+
}, [backend, pollMs, project, theme, version]);
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (!state.streaming) return;
|
|
708
|
+
const timer = setInterval(() => setClock(now()), 250);
|
|
709
|
+
return () => clearInterval(timer);
|
|
710
|
+
}, [state.streaming, now]);
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (!notice) return;
|
|
713
|
+
const timer = setTimeout(() => setNotice(void 0), EXIT_WINDOW_MS);
|
|
714
|
+
return () => clearTimeout(timer);
|
|
715
|
+
}, [notice]);
|
|
716
|
+
const refreshQuota = useCallback(
|
|
717
|
+
async (accountId) => {
|
|
718
|
+
try {
|
|
719
|
+
const usage = (await backend.usage()).find((u) => u.accountId === accountId);
|
|
720
|
+
const window = usage?.windows["5h"];
|
|
721
|
+
const limited = window && (window.tokenLimit !== void 0 || window.requestLimit !== void 0);
|
|
722
|
+
dispatch({
|
|
723
|
+
type: "quota",
|
|
724
|
+
accountId,
|
|
725
|
+
utilization: limited ? window.utilization : void 0
|
|
726
|
+
});
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
[backend]
|
|
731
|
+
);
|
|
732
|
+
const send = useCallback(
|
|
733
|
+
async (text2) => {
|
|
734
|
+
dispatch({ type: "user", text: text2 });
|
|
735
|
+
const conversation = [
|
|
736
|
+
...stateRef.current.conversation,
|
|
737
|
+
{ role: "user", content: text2 }
|
|
738
|
+
];
|
|
739
|
+
const controller = new AbortController();
|
|
740
|
+
abortRef.current = controller;
|
|
741
|
+
dispatch({ type: "stream-start", now: now() });
|
|
742
|
+
let usage;
|
|
743
|
+
let accountId;
|
|
744
|
+
try {
|
|
745
|
+
const { meta, chunks } = await backend.stream(
|
|
746
|
+
{ model: stateRef.current.model, messages: conversation },
|
|
747
|
+
controller.signal
|
|
748
|
+
);
|
|
749
|
+
accountId = meta.accountId;
|
|
750
|
+
dispatch({
|
|
751
|
+
type: "stream-meta",
|
|
752
|
+
failovers: meta.failovers,
|
|
753
|
+
...meta.accountId ? { accountId: meta.accountId } : {},
|
|
754
|
+
...meta.provider ? { provider: meta.provider } : {},
|
|
755
|
+
...meta.accountId && labels.current.has(meta.accountId) ? { accountLabel: labels.current.get(meta.accountId) } : {}
|
|
756
|
+
});
|
|
757
|
+
dispatch({ type: "connection", connection: "online" });
|
|
758
|
+
for await (const chunk of chunks) {
|
|
759
|
+
if (controller.signal.aborted) break;
|
|
760
|
+
const delta = chunk.choices[0]?.delta?.content;
|
|
761
|
+
if (typeof delta === "string" && delta) dispatch({ type: "stream-delta", text: delta });
|
|
762
|
+
if (chunk.usage) usage = chunk.usage;
|
|
763
|
+
}
|
|
764
|
+
dispatch({
|
|
765
|
+
type: "stream-end",
|
|
766
|
+
now: now(),
|
|
767
|
+
...usage ? { usage } : {},
|
|
768
|
+
...controller.signal.aborted ? { cancelled: true } : {}
|
|
769
|
+
});
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (controller.signal.aborted) {
|
|
772
|
+
dispatch({ type: "stream-end", now: now(), cancelled: true });
|
|
773
|
+
} else {
|
|
774
|
+
const status = err.status;
|
|
775
|
+
if (status === 0) dispatch({ type: "connection", connection: "offline" });
|
|
776
|
+
let text3 = errorText(err);
|
|
777
|
+
if (connect)
|
|
778
|
+
text3 = text3.replace(/Run: davecode accounts login \S+/, "Type /connect to sign in.");
|
|
779
|
+
dispatch({ type: "stream-end", now: now(), error: text3 });
|
|
780
|
+
}
|
|
781
|
+
} finally {
|
|
782
|
+
if (abortRef.current === controller) abortRef.current = void 0;
|
|
783
|
+
}
|
|
784
|
+
if (accountId) void refreshQuota(accountId);
|
|
785
|
+
},
|
|
786
|
+
[backend, connect, now, refreshQuota]
|
|
787
|
+
);
|
|
788
|
+
const runSlash = useCallback(
|
|
789
|
+
async (input) => {
|
|
790
|
+
const parsed = parseSlash(input);
|
|
791
|
+
if (parsed.kind === "unknown") {
|
|
792
|
+
dispatch({
|
|
793
|
+
type: "error",
|
|
794
|
+
text: `Unknown command /${parsed.name}. Type /help for the list.`
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (parsed.kind !== "command") return;
|
|
799
|
+
const { command, args } = parsed;
|
|
800
|
+
try {
|
|
801
|
+
switch (command.name) {
|
|
802
|
+
case "help":
|
|
803
|
+
dispatch({ type: "info", text: helpText(theme) });
|
|
804
|
+
break;
|
|
805
|
+
case "exit":
|
|
806
|
+
abortRef.current?.abort();
|
|
807
|
+
exit();
|
|
808
|
+
break;
|
|
809
|
+
case "clear":
|
|
810
|
+
abortRef.current?.abort();
|
|
811
|
+
write("\x1B[2J\x1B[3J\x1B[H");
|
|
812
|
+
dispatch({ type: "clear" });
|
|
813
|
+
break;
|
|
814
|
+
case "model": {
|
|
815
|
+
const choose = (id) => {
|
|
816
|
+
dispatch({ type: "set-model", model: id });
|
|
817
|
+
dispatch({ type: "info", text: `Model set to ${theme.accent(id)}` });
|
|
818
|
+
};
|
|
819
|
+
if (args) {
|
|
820
|
+
choose(args);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
const ids = (await backend.models()).map((m) => m.id).sort(
|
|
824
|
+
(a, b) => Number(!a.startsWith("davecode/")) - Number(!b.startsWith("davecode/")) || a.localeCompare(b)
|
|
825
|
+
);
|
|
826
|
+
if (ids.length === 0) {
|
|
827
|
+
dispatch({
|
|
828
|
+
type: "info",
|
|
829
|
+
text: await modelsText(theme, backend, stateRef.current.model)
|
|
830
|
+
});
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
const current = stateRef.current.model;
|
|
834
|
+
setPicker({
|
|
835
|
+
title: "Choose a model",
|
|
836
|
+
options: ids.map((id) => ({
|
|
837
|
+
value: id,
|
|
838
|
+
label: id,
|
|
839
|
+
...id === current ? { hint: "(current)" } : {}
|
|
840
|
+
})),
|
|
841
|
+
index: Math.max(0, ids.indexOf(current)),
|
|
842
|
+
pick: choose
|
|
843
|
+
});
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case "connect": {
|
|
847
|
+
if (!connect) {
|
|
848
|
+
dispatch({ type: "info", text: "Connect agents with davecode accounts add." });
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
setPicker({
|
|
852
|
+
title: "Connect an agent",
|
|
853
|
+
options: await connect.agents(),
|
|
854
|
+
index: 0,
|
|
855
|
+
pick: (provider) => {
|
|
856
|
+
abortRef.current?.abort();
|
|
857
|
+
connect.choose(provider);
|
|
858
|
+
exit();
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
case "route":
|
|
864
|
+
dispatch({ type: "info", text: await routesText(theme, backend) });
|
|
865
|
+
break;
|
|
866
|
+
case "accounts": {
|
|
867
|
+
const accounts = await backend.accounts();
|
|
868
|
+
for (const a of accounts) labels.current.set(a.id, a.label);
|
|
869
|
+
dispatch({ type: "info", text: await accountsText(theme, backend, width - 4) });
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
case "tasks":
|
|
873
|
+
dispatch({ type: "info", text: await tasksText(theme, backend, width - 4) });
|
|
874
|
+
break;
|
|
875
|
+
case "status":
|
|
876
|
+
dispatch({
|
|
877
|
+
type: "info",
|
|
878
|
+
text: await statusText(theme, backend, stateRef.current.model)
|
|
879
|
+
});
|
|
880
|
+
break;
|
|
881
|
+
case "login":
|
|
882
|
+
case "logout":
|
|
883
|
+
case "account": {
|
|
884
|
+
if (!account) {
|
|
885
|
+
dispatch({
|
|
886
|
+
type: "info",
|
|
887
|
+
text: "DaveCode accounts are not available in this build."
|
|
888
|
+
});
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
const text2 = command.name === "login" ? await account.login((note) => dispatch({ type: "info", text: theme.dim(note) })) : command.name === "logout" ? await account.logout() : await account.status();
|
|
892
|
+
dispatch({ type: "info", text: text2 });
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
} catch (err) {
|
|
897
|
+
dispatch({ type: "error", text: `/${command.name} failed: ${errorText(err)}` });
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
[account, backend, connect, exit, theme, width, write]
|
|
901
|
+
);
|
|
902
|
+
const submit = useCallback(() => {
|
|
903
|
+
const text2 = textOf(editor).trim();
|
|
904
|
+
if (!text2) return;
|
|
905
|
+
if (parseSlash(text2).kind !== "none") {
|
|
906
|
+
setEditor(emptyEditor);
|
|
907
|
+
setHistoryIndex(void 0);
|
|
908
|
+
void runSlash(text2);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (stateRef.current.streaming) {
|
|
912
|
+
setNotice("Wait for the reply, or press Esc to cancel it");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
setEditor(emptyEditor);
|
|
916
|
+
setHistoryIndex(void 0);
|
|
917
|
+
void send(text2);
|
|
918
|
+
}, [editor, runSlash, send]);
|
|
919
|
+
const recall = (direction) => {
|
|
920
|
+
const { history } = stateRef.current;
|
|
921
|
+
if (history.length === 0) return;
|
|
922
|
+
const current = historyIndex ?? history.length;
|
|
923
|
+
const next = Math.min(history.length, Math.max(0, current + direction));
|
|
924
|
+
setHistoryIndex(next === history.length ? void 0 : next);
|
|
925
|
+
setEditor(next === history.length ? emptyEditor : fromText(history[next]));
|
|
926
|
+
};
|
|
927
|
+
useInput((input, key) => {
|
|
928
|
+
if (key.ctrl && input === "c") {
|
|
929
|
+
const armed = exitArmedAt.current !== void 0 && now() - exitArmedAt.current < EXIT_WINDOW_MS;
|
|
930
|
+
if (armed) {
|
|
931
|
+
abortRef.current?.abort();
|
|
932
|
+
exit();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
exitArmedAt.current = now();
|
|
936
|
+
if (stateRef.current.streaming) abortRef.current?.abort();
|
|
937
|
+
else if (editor.chars.length > 0) setEditor(emptyEditor);
|
|
938
|
+
setNotice("Press Ctrl+C again to exit");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
exitArmedAt.current = void 0;
|
|
942
|
+
if (picker) {
|
|
943
|
+
const count = picker.options.length;
|
|
944
|
+
const move = (index) => setPicker({ ...picker, index });
|
|
945
|
+
if (key.escape) return setPicker(void 0);
|
|
946
|
+
if (key.upArrow) return move((picker.index - 1 + count) % count);
|
|
947
|
+
if (key.downArrow) return move((picker.index + 1) % count);
|
|
948
|
+
const digit = count <= 9 && /^[1-9]$/.test(input) ? Number(input) - 1 : void 0;
|
|
949
|
+
if (key.return || digit !== void 0 && digit < count) {
|
|
950
|
+
const option = picker.options[digit ?? picker.index];
|
|
951
|
+
setPicker(void 0);
|
|
952
|
+
if (option) picker.pick(option.value);
|
|
953
|
+
}
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const menu = slashSuggestions(textOf(editor));
|
|
957
|
+
if (menu.length > 0) {
|
|
958
|
+
const selected = menu[Math.min(slashIndex, menu.length - 1)];
|
|
959
|
+
if (key.upArrow) return setSlashIndex((i) => (i - 1 + menu.length) % menu.length);
|
|
960
|
+
if (key.downArrow) return setSlashIndex((i) => (i + 1) % menu.length);
|
|
961
|
+
if (key.escape) return setEditor(emptyEditor);
|
|
962
|
+
if (key.tab) {
|
|
963
|
+
setEditor(fromText(`/${selected.name}${selected.args ? " " : ""}`));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (key.return && !key.shift && !key.meta) {
|
|
967
|
+
setEditor(emptyEditor);
|
|
968
|
+
setHistoryIndex(void 0);
|
|
969
|
+
void runSlash(`/${selected.name}`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (key.escape) {
|
|
974
|
+
if (stateRef.current.streaming) abortRef.current?.abort();
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (input === "\n" && !key.return || key.ctrl && input === "j") {
|
|
978
|
+
setEditor((e) => insert(e, "\n"));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (key.return) {
|
|
982
|
+
if (key.shift || key.meta) setEditor((e) => insert(e, "\n"));
|
|
983
|
+
else if (continuesLine(editor)) setEditor((e) => newlineAfterBackslash(e));
|
|
984
|
+
else submit();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (key.tab) return;
|
|
988
|
+
if (key.upArrow) {
|
|
989
|
+
const moved = up(editor);
|
|
990
|
+
if (moved) setEditor(moved);
|
|
991
|
+
else recall(-1);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (key.downArrow) {
|
|
995
|
+
const moved = down(editor);
|
|
996
|
+
if (moved) setEditor(moved);
|
|
997
|
+
else if (historyIndex !== void 0) recall(1);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (key.leftArrow) return setEditor(left);
|
|
1001
|
+
if (key.rightArrow) return setEditor(right);
|
|
1002
|
+
if (key.home || key.ctrl && input === "a") return setEditor(lineStart);
|
|
1003
|
+
if (key.end || key.ctrl && input === "e") return setEditor(lineEnd);
|
|
1004
|
+
if (key.backspace) return setEditor(backspace);
|
|
1005
|
+
if (key.delete) return setEditor(deleteForward);
|
|
1006
|
+
if (key.ctrl && input === "u") return setEditor(killToLineStart);
|
|
1007
|
+
if (key.ctrl && input === "k") return setEditor(killToLineEnd);
|
|
1008
|
+
if (key.ctrl && input === "w") return setEditor(deleteWordBack);
|
|
1009
|
+
if (key.ctrl && input === "l") {
|
|
1010
|
+
write("\x1B[2J\x1B[H");
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (key.ctrl || key.meta || !input) return;
|
|
1014
|
+
setEditor((e) => insert(e, input));
|
|
1015
|
+
});
|
|
1016
|
+
usePaste((text2) => {
|
|
1017
|
+
setEditor((e) => insert(e, text2));
|
|
1018
|
+
});
|
|
1019
|
+
const text = textOf(editor);
|
|
1020
|
+
const suggestions = slashSuggestions(text);
|
|
1021
|
+
useEffect(() => setSlashIndex(0), [text]);
|
|
1022
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
|
|
1023
|
+
/* @__PURE__ */ jsx5(Static, { items: state.entries, children: (entry) => /* @__PURE__ */ jsx5(EntryView, { entry }, entry.id) }, state.epoch),
|
|
1024
|
+
state.streaming ? /* @__PURE__ */ jsx5(StreamingView, { streaming: state.streaming, now: clock }) : null,
|
|
1025
|
+
picker ? /* @__PURE__ */ jsx5(Picker, { title: picker.title, options: picker.options, index: picker.index }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
1026
|
+
/* @__PURE__ */ jsx5(InputBox, { editor, width, placeholder: PLACEHOLDER }),
|
|
1027
|
+
/* @__PURE__ */ jsx5(
|
|
1028
|
+
SlashHints,
|
|
1029
|
+
{
|
|
1030
|
+
commands: suggestions,
|
|
1031
|
+
width,
|
|
1032
|
+
selected: Math.min(slashIndex, Math.max(0, suggestions.length - 1))
|
|
1033
|
+
}
|
|
1034
|
+
)
|
|
1035
|
+
] }),
|
|
1036
|
+
!picker && suggestions.length === 0 ? /* @__PURE__ */ jsx5(StatusLine, { state, width, info: notice ? { notice } : {} }) : null
|
|
1037
|
+
] });
|
|
1038
|
+
}
|
|
1039
|
+
function ChatApp(props) {
|
|
1040
|
+
return /* @__PURE__ */ jsx5(ThemeProvider, { theme: props.theme, children: /* @__PURE__ */ jsx5(Chat, { ...props }) });
|
|
1041
|
+
}
|
|
1042
|
+
export {
|
|
1043
|
+
ChatApp
|
|
1044
|
+
};
|
|
1045
|
+
//# sourceMappingURL=ChatApp-BO7QMBWD.js.map
|