darkfoo-code 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +289 -249
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2068,8 +2068,8 @@ function App({ model, systemPromptOverride, maxTurns, initialMessages, initialSe
2068
2068
  }
2069
2069
 
2070
2070
  // src/repl.tsx
2071
- import { useState as useState2, useCallback as useCallback2, useEffect, useRef, useMemo } from "react";
2072
- import { Box as Box6, Text as Text6, useApp, useInput as useInput2 } from "ink";
2071
+ import { useState as useState3, useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useMemo } from "react";
2072
+ import { Box as Box6, Text as Text6, Static, useApp, useInput as useInput2 } from "ink";
2073
2073
  import { nanoid as nanoid6 } from "nanoid";
2074
2074
 
2075
2075
  // src/components/Banner.tsx
@@ -2077,7 +2077,7 @@ init_theme();
2077
2077
  import { memo } from "react";
2078
2078
  import { Box, Text } from "ink";
2079
2079
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
2080
- var version = "0.4.1";
2080
+ var version = "0.4.3";
2081
2081
  var LOGO_LINES = [
2082
2082
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
2083
2083
  " \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557",
@@ -2162,107 +2162,56 @@ var Banner = memo(function Banner2({ model, cwd, providerName, providerOnline, m
2162
2162
  ] });
2163
2163
  });
2164
2164
 
2165
- // src/components/Messages.tsx
2165
+ // src/components/StreamingDisplay.tsx
2166
2166
  init_theme();
2167
- init_format();
2168
- import { memo as memo2 } from "react";
2167
+ import { useState, useRef, useEffect, useImperativeHandle, forwardRef, memo as memo2 } from "react";
2169
2168
  import { Box as Box2, Text as Text2 } from "ink";
2170
-
2171
- // src/utils/markdown.ts
2172
- function renderMarkdown(text) {
2173
- const lines = text.split("\n");
2174
- const result = [];
2175
- let inCodeBlock = false;
2176
- let codeBlockLang = "";
2177
- for (const line of lines) {
2178
- if (line.trimStart().startsWith("```")) {
2179
- if (!inCodeBlock) {
2180
- codeBlockLang = line.trimStart().slice(3).trim();
2181
- const label = codeBlockLang ? ` ${codeBlockLang}` : "";
2182
- result.push(`\x1B[2m\x1B[38;2;94;234;212m\u2500\u2500\u2500 code${label} \u2500\u2500\u2500\x1B[0m`);
2183
- inCodeBlock = true;
2184
- } else {
2185
- result.push(`\x1B[2m\x1B[38;2;94;234;212m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m`);
2186
- inCodeBlock = false;
2187
- codeBlockLang = "";
2188
- }
2189
- continue;
2190
- }
2191
- if (inCodeBlock) {
2192
- result.push(`\x1B[38;2;251;191;36m ${line}\x1B[0m`);
2193
- continue;
2194
- }
2195
- const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
2196
- if (headerMatch) {
2197
- const level = headerMatch[1].length;
2198
- const text2 = headerMatch[2];
2199
- if (level <= 2) {
2200
- result.push(`\x1B[1m\x1B[38;2;94;234;212m${text2}\x1B[0m`);
2201
- } else {
2202
- result.push(`\x1B[1m${text2}\x1B[0m`);
2203
- }
2204
- continue;
2205
- }
2206
- const listMatch = line.match(/^(\s*)[*-]\s+(.+)/);
2207
- if (listMatch) {
2208
- const indent = listMatch[1] || "";
2209
- const content = renderInline(listMatch[2]);
2210
- result.push(`${indent}\x1B[38;2;94;234;212m\u2022\x1B[0m ${content}`);
2211
- continue;
2212
- }
2213
- const orderedMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
2214
- if (orderedMatch) {
2215
- const indent = orderedMatch[1] || "";
2216
- const content = renderInline(orderedMatch[2]);
2217
- result.push(`${indent}${content}`);
2218
- continue;
2219
- }
2220
- if (/^---+$|^\*\*\*+$|^___+$/.test(line.trim())) {
2221
- result.push(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`);
2222
- continue;
2223
- }
2224
- result.push(renderInline(line));
2225
- }
2226
- return result.join("\n");
2227
- }
2228
- function renderInline(text) {
2229
- return text.replace(/`([^`]+)`/g, "\x1B[38;2;251;191;36m$1\x1B[0m").replace(/\*\*\*(.+?)\*\*\*/g, "\x1B[1m\x1B[3m$1\x1B[0m").replace(/\*\*(.+?)\*\*/g, "\x1B[1m$1\x1B[0m").replace(/\*(.+?)\*/g, "\x1B[3m$1\x1B[0m").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1B[4m\x1B[38;2;94;234;212m$1\x1B[0m");
2230
- }
2231
-
2232
- // src/components/Messages.tsx
2233
2169
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2234
- var Messages = memo2(function Messages2({ messages }) {
2235
- return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx3(MessageRow, { message: msg }, msg.id)) });
2236
- });
2237
- var MessageRow = memo2(function MessageRow2({ message }) {
2238
- switch (message.role) {
2239
- case "user":
2240
- return /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, marginBottom: 1, children: [
2241
- /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, bold: true, children: "\u276F " }),
2242
- /* @__PURE__ */ jsx3(Text2, { bold: true, color: theme.text, children: message.content })
2243
- ] });
2244
- case "assistant": {
2245
- if (!message.content && message.toolCalls) return null;
2246
- if (!message.content) return null;
2247
- const rendered = renderMarkdown(message.content);
2248
- return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs2(Box2, { children: [
2249
- /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, children: "\u23BF " }),
2250
- /* @__PURE__ */ jsx3(Text2, { wrap: "wrap", children: rendered })
2251
- ] }) });
2252
- }
2253
- case "tool": {
2254
- const lines = message.content.split("\n");
2255
- const preview = lines.length > 8 ? lines.slice(0, 8).join("\n") + `
2256
- ... (${lines.length - 8} more lines)` : message.content;
2257
- return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
2258
- "\u23BF ",
2259
- truncate(preview, 1200)
2260
- ] }) });
2170
+ var FLUSH_INTERVAL_MS = 150;
2171
+ var StreamingDisplay = memo2(forwardRef(function StreamingDisplay2(_, ref) {
2172
+ const bufferRef = useRef("");
2173
+ const [displayText, setDisplayText] = useState("");
2174
+ const timerRef = useRef(null);
2175
+ function startTimer() {
2176
+ if (timerRef.current) return;
2177
+ timerRef.current = setInterval(() => {
2178
+ const buf = bufferRef.current;
2179
+ if (!buf) return;
2180
+ const lastNL = buf.lastIndexOf("\n");
2181
+ setDisplayText(lastNL >= 0 ? buf.substring(0, lastNL + 1) : buf);
2182
+ }, FLUSH_INTERVAL_MS);
2183
+ }
2184
+ function stopTimer() {
2185
+ if (timerRef.current) {
2186
+ clearInterval(timerRef.current);
2187
+ timerRef.current = null;
2188
+ }
2189
+ }
2190
+ useImperativeHandle(ref, () => ({
2191
+ append(text) {
2192
+ bufferRef.current += text;
2193
+ startTimer();
2194
+ },
2195
+ clear() {
2196
+ stopTimer();
2197
+ if (bufferRef.current) {
2198
+ setDisplayText("");
2199
+ }
2200
+ bufferRef.current = "";
2201
+ },
2202
+ getText() {
2203
+ return bufferRef.current;
2261
2204
  }
2262
- default:
2263
- return null;
2264
- }
2265
- });
2205
+ }));
2206
+ useEffect(() => {
2207
+ return () => stopTimer();
2208
+ }, []);
2209
+ if (!displayText) return null;
2210
+ return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
2211
+ /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, children: "\u23BF " }),
2212
+ /* @__PURE__ */ jsx3(Text2, { color: theme.text, wrap: "wrap", children: displayText })
2213
+ ] });
2214
+ }));
2266
2215
 
2267
2216
  // src/components/ToolCall.tsx
2268
2217
  init_theme();
@@ -2380,7 +2329,7 @@ var StatusLine = memo4(function StatusLine2({ model, messageCount, tokenEstimate
2380
2329
 
2381
2330
  // src/components/UserInput.tsx
2382
2331
  init_theme();
2383
- import { useState, useCallback, memo as memo5 } from "react";
2332
+ import { useState as useState2, useCallback, memo as memo5 } from "react";
2384
2333
  import { Box as Box5, Text as Text5, useInput } from "ink";
2385
2334
  import TextInput from "ink-text-input";
2386
2335
 
@@ -3419,8 +3368,8 @@ function getCommandNames() {
3419
3368
  // src/components/UserInput.tsx
3420
3369
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3421
3370
  var UserInput = memo5(function UserInput2({ value, onChange, onSubmit, disabled, history }) {
3422
- const [historyIdx, setHistoryIdx] = useState(-1);
3423
- const [suggestion, setSuggestion] = useState("");
3371
+ const [historyIdx, setHistoryIdx] = useState2(-1);
3372
+ const [suggestion, setSuggestion] = useState2("");
3424
3373
  useInput((_input, key) => {
3425
3374
  if (disabled || !history || history.length === 0) return;
3426
3375
  if (key.upArrow) {
@@ -3509,7 +3458,71 @@ init_tools();
3509
3458
  init_bash();
3510
3459
  init_hooks();
3511
3460
  init_theme();
3512
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3461
+
3462
+ // src/utils/markdown.ts
3463
+ function renderMarkdown(text) {
3464
+ const lines = text.split("\n");
3465
+ const result = [];
3466
+ let inCodeBlock = false;
3467
+ let codeBlockLang = "";
3468
+ for (const line of lines) {
3469
+ if (line.trimStart().startsWith("```")) {
3470
+ if (!inCodeBlock) {
3471
+ codeBlockLang = line.trimStart().slice(3).trim();
3472
+ const label = codeBlockLang ? ` ${codeBlockLang}` : "";
3473
+ result.push(`\x1B[2m\x1B[38;2;94;234;212m\u2500\u2500\u2500 code${label} \u2500\u2500\u2500\x1B[0m`);
3474
+ inCodeBlock = true;
3475
+ } else {
3476
+ result.push(`\x1B[2m\x1B[38;2;94;234;212m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m`);
3477
+ inCodeBlock = false;
3478
+ codeBlockLang = "";
3479
+ }
3480
+ continue;
3481
+ }
3482
+ if (inCodeBlock) {
3483
+ result.push(`\x1B[38;2;251;191;36m ${line}\x1B[0m`);
3484
+ continue;
3485
+ }
3486
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
3487
+ if (headerMatch) {
3488
+ const level = headerMatch[1].length;
3489
+ const text2 = headerMatch[2];
3490
+ if (level <= 2) {
3491
+ result.push(`\x1B[1m\x1B[38;2;94;234;212m${text2}\x1B[0m`);
3492
+ } else {
3493
+ result.push(`\x1B[1m${text2}\x1B[0m`);
3494
+ }
3495
+ continue;
3496
+ }
3497
+ const listMatch = line.match(/^(\s*)[*-]\s+(.+)/);
3498
+ if (listMatch) {
3499
+ const indent = listMatch[1] || "";
3500
+ const content = renderInline(listMatch[2]);
3501
+ result.push(`${indent}\x1B[38;2;94;234;212m\u2022\x1B[0m ${content}`);
3502
+ continue;
3503
+ }
3504
+ const orderedMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
3505
+ if (orderedMatch) {
3506
+ const indent = orderedMatch[1] || "";
3507
+ const content = renderInline(orderedMatch[2]);
3508
+ result.push(`${indent}${content}`);
3509
+ continue;
3510
+ }
3511
+ if (/^---+$|^\*\*\*+$|^___+$/.test(line.trim())) {
3512
+ result.push(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`);
3513
+ continue;
3514
+ }
3515
+ result.push(renderInline(line));
3516
+ }
3517
+ return result.join("\n");
3518
+ }
3519
+ function renderInline(text) {
3520
+ return text.replace(/`([^`]+)`/g, "\x1B[38;2;251;191;36m$1\x1B[0m").replace(/\*\*\*(.+?)\*\*\*/g, "\x1B[1m\x1B[3m$1\x1B[0m").replace(/\*\*(.+?)\*\*/g, "\x1B[1m$1\x1B[0m").replace(/\*(.+?)\*/g, "\x1B[3m$1\x1B[0m").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1B[4m\x1B[38;2;94;234;212m$1\x1B[0m");
3521
+ }
3522
+
3523
+ // src/repl.tsx
3524
+ init_format();
3525
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3513
3526
  function getContextLimit3(model) {
3514
3527
  const lower = model.toLowerCase();
3515
3528
  if (lower.includes("llama3.1")) return 131072;
@@ -3520,105 +3533,150 @@ function getContextLimit3(model) {
3520
3533
  if (lower.includes("deepseek")) return 32768;
3521
3534
  return 8192;
3522
3535
  }
3523
- var STREAM_THROTTLE_MS = 80;
3536
+ function FrozenItemView({ item }) {
3537
+ switch (item.kind) {
3538
+ case "banner":
3539
+ return /* @__PURE__ */ jsx7(Banner, { model: item.model, cwd: item.cwd, providerName: item.providerName, providerOnline: item.providerOnline });
3540
+ case "user":
3541
+ return /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, marginBottom: 1, children: [
3542
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, bold: true, children: "\u276F " }),
3543
+ /* @__PURE__ */ jsx7(Text6, { bold: true, color: theme.text, children: item.content })
3544
+ ] });
3545
+ case "assistant": {
3546
+ if (!item.content) return null;
3547
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs6(Box6, { children: [
3548
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "\u23BF " }),
3549
+ /* @__PURE__ */ jsx7(Text6, { wrap: "wrap", children: renderMarkdown(item.content) })
3550
+ ] }) });
3551
+ }
3552
+ case "tool": {
3553
+ const lines = item.content.split("\n");
3554
+ const preview = lines.length > 8 ? lines.slice(0, 8).join("\n") + `
3555
+ ... (${lines.length - 8} more lines)` : item.content;
3556
+ return /* @__PURE__ */ jsx7(Box6, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
3557
+ "\u23BF ",
3558
+ truncate(preview, 1200)
3559
+ ] }) });
3560
+ }
3561
+ case "tool-result": {
3562
+ const icon = item.isError ? "\u2718" : "\u2714";
3563
+ const iconColor = item.isError ? theme.pink : theme.green;
3564
+ const lines = item.output.split("\n");
3565
+ const preview = lines.length > 6 ? lines.slice(0, 6).join("\n") + `
3566
+ ... (${lines.length - 6} more lines)` : item.output;
3567
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
3568
+ /* @__PURE__ */ jsxs6(Box6, { children: [
3569
+ /* @__PURE__ */ jsxs6(Text6, { color: iconColor, children: [
3570
+ icon,
3571
+ " "
3572
+ ] }),
3573
+ /* @__PURE__ */ jsx7(Text6, { bold: true, color: theme.yellow, children: item.toolName })
3574
+ ] }),
3575
+ /* @__PURE__ */ jsx7(Box6, { marginLeft: 2, children: /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
3576
+ "\u23BF ",
3577
+ truncate(preview, 1200)
3578
+ ] }) })
3579
+ ] });
3580
+ }
3581
+ case "info":
3582
+ return /* @__PURE__ */ jsx7(Box6, { marginBottom: 1, marginLeft: 2, children: /* @__PURE__ */ jsx7(Text6, { children: item.text }) });
3583
+ default:
3584
+ return null;
3585
+ }
3586
+ }
3524
3587
  function REPL({ initialPrompt }) {
3525
3588
  const { model: initialModel, systemPromptOverride, maxTurns, initialMessages, initialSessionId } = useDarkfooContext();
3526
3589
  const { exit } = useApp();
3527
- const [model, setModel] = useState2(initialModel);
3528
- const [messages, setMessages] = useState2(initialMessages ?? []);
3529
- const [inputValue, setInputValue] = useState2("");
3530
- const [isStreaming, setIsStreaming] = useState2(false);
3531
- const [streamingText, setStreamingText] = useState2("");
3532
- const [activeTool, setActiveTool] = useState2(null);
3533
- const [toolResults, setToolResults] = useState2([]);
3534
- const [commandOutput, setCommandOutput] = useState2(null);
3535
- const [inputHistory, setInputHistory] = useState2([]);
3536
- const [tokenCounts, setTokenCounts] = useState2({ input: 0, output: 0 });
3537
- const [systemPrompt, setSystemPrompt] = useState2("");
3538
- const [mascotMood, setMascotMood] = useState2("idle");
3539
- const [providerOnline, setProviderOnline] = useState2(true);
3540
- const abortRef = useRef(null);
3541
- const hasRun = useRef(false);
3542
- const sessionId = useRef(initialSessionId ?? createSessionId());
3543
- const streamBufferRef = useRef("");
3544
- const streamFlushTimer = useRef(null);
3545
- const startStreamFlush = useCallback2(() => {
3546
- if (streamFlushTimer.current) return;
3547
- streamFlushTimer.current = setInterval(() => {
3548
- const buf = streamBufferRef.current;
3549
- if (buf) {
3550
- setStreamingText(buf);
3551
- }
3552
- }, STREAM_THROTTLE_MS);
3553
- }, []);
3554
- const stopStreamFlush = useCallback2(() => {
3555
- if (streamFlushTimer.current) {
3556
- clearInterval(streamFlushTimer.current);
3557
- streamFlushTimer.current = null;
3558
- }
3559
- if (streamBufferRef.current) {
3560
- setStreamingText(streamBufferRef.current);
3561
- }
3562
- }, []);
3563
- useEffect(() => {
3564
- return () => {
3565
- if (streamFlushTimer.current) clearInterval(streamFlushTimer.current);
3566
- };
3567
- }, []);
3568
- const messagesRef = useRef(messages);
3590
+ const [model, setModel] = useState3(initialModel);
3591
+ const [messages, setMessages] = useState3(initialMessages ?? []);
3592
+ const [inputValue, setInputValue] = useState3("");
3593
+ const [isStreaming, setIsStreaming] = useState3(false);
3594
+ const [receivedText, setReceivedText] = useState3(false);
3595
+ const [activeTool, setActiveTool] = useState3(null);
3596
+ const [commandOutput, setCommandOutput] = useState3(null);
3597
+ const [inputHistory, setInputHistory] = useState3([]);
3598
+ const [tokenCounts, setTokenCounts] = useState3({ input: 0, output: 0 });
3599
+ const [systemPrompt, setSystemPrompt] = useState3("");
3600
+ const [providerOnline, setProviderOnline] = useState3(true);
3601
+ const abortRef = useRef2(null);
3602
+ const hasRun = useRef2(false);
3603
+ const sessionId = useRef2(initialSessionId ?? createSessionId());
3604
+ const [frozen, setFrozen] = useState3([]);
3605
+ const frozenBanner = useRef2(false);
3606
+ const streamingRef = useRef2(null);
3607
+ const messagesRef = useRef2(messages);
3569
3608
  messagesRef.current = messages;
3570
- const modelRef = useRef(model);
3609
+ const modelRef = useRef2(model);
3571
3610
  modelRef.current = model;
3572
- const systemPromptRef = useRef(systemPrompt);
3611
+ const systemPromptRef = useRef2(systemPrompt);
3573
3612
  systemPromptRef.current = systemPrompt;
3574
3613
  const tools = getTools();
3575
3614
  const cwd = process.cwd();
3576
3615
  const providerName = useMemo(() => getActiveProviderName(), [model, providerOnline]);
3577
- useEffect(() => {
3616
+ const freeze = useCallback2((item) => {
3617
+ setFrozen((prev) => [...prev, item]);
3618
+ }, []);
3619
+ useEffect2(() => {
3578
3620
  if (systemPromptOverride) {
3579
3621
  setSystemPrompt(systemPromptOverride);
3580
3622
  } else {
3581
3623
  buildSystemPrompt(tools, cwd).then(setSystemPrompt);
3582
3624
  }
3583
3625
  }, []);
3584
- useEffect(() => {
3626
+ useEffect2(() => {
3585
3627
  executeHooks("session_start", { cwd }).catch(() => {
3586
3628
  });
3587
3629
  }, []);
3588
- useEffect(() => {
3630
+ useEffect2(() => {
3589
3631
  const provider = getProvider();
3590
3632
  provider.healthCheck().then((ok) => {
3591
3633
  setProviderOnline(ok);
3592
3634
  if (!ok) {
3593
3635
  setModel("");
3636
+ if (!frozenBanner.current) {
3637
+ frozenBanner.current = true;
3638
+ freeze({ id: "banner", kind: "banner", model: "", cwd, providerName: getActiveProviderName(), providerOnline: false });
3639
+ }
3594
3640
  return;
3595
3641
  }
3596
- provider.listModels().then((models) => {
3597
- if (models.length === 0) {
3598
- setModel("");
3599
- return;
3642
+ provider.listModels().then((resolvedModels) => {
3643
+ let resolved = "";
3644
+ if (resolvedModels.length > 0) {
3645
+ const requested = model.toLowerCase();
3646
+ const match = resolvedModels.find((m) => m.name.toLowerCase() === requested);
3647
+ if (match) {
3648
+ resolved = match.name;
3649
+ } else {
3650
+ const preferred = resolvedModels.find((m) => m.name.includes("llama3.1")) ?? resolvedModels.find((m) => m.name.includes("qwen")) ?? resolvedModels.find((m) => m.name.includes("instruct")) ?? resolvedModels[0];
3651
+ if (preferred) {
3652
+ resolved = preferred.name;
3653
+ freeze({ id: nanoid6(), kind: "info", text: `Model "${model}" not found. Using ${preferred.name} instead.` });
3654
+ }
3655
+ }
3600
3656
  }
3601
- const requested = model.toLowerCase();
3602
- const match = models.find((m) => m.name.toLowerCase() === requested);
3603
- if (match) {
3604
- setModel(match.name);
3605
- return;
3657
+ setModel(resolved);
3658
+ if (!frozenBanner.current) {
3659
+ frozenBanner.current = true;
3660
+ freeze({ id: "banner", kind: "banner", model: resolved, cwd, providerName: getActiveProviderName(), providerOnline: true });
3606
3661
  }
3607
- const preferred = models.find((m) => m.name.includes("llama3.1")) ?? models.find((m) => m.name.includes("qwen")) ?? models.find((m) => m.name.includes("instruct")) ?? models[0];
3608
- if (preferred) {
3609
- setModel(preferred.name);
3610
- setCommandOutput(`Model "${model}" not found. Using ${preferred.name} instead.`);
3611
- } else {
3612
- setModel("");
3662
+ }).catch(() => {
3663
+ setModel("");
3664
+ if (!frozenBanner.current) {
3665
+ frozenBanner.current = true;
3666
+ freeze({ id: "banner", kind: "banner", model: "", cwd, providerName: getActiveProviderName(), providerOnline: false });
3613
3667
  }
3614
- }).catch(() => setModel(""));
3668
+ });
3615
3669
  }).catch(() => {
3616
3670
  setProviderOnline(false);
3617
3671
  setModel("");
3672
+ if (!frozenBanner.current) {
3673
+ frozenBanner.current = true;
3674
+ freeze({ id: "banner", kind: "banner", model: "", cwd, providerName: getActiveProviderName(), providerOnline: false });
3675
+ }
3618
3676
  });
3619
3677
  }, []);
3620
3678
  const clearMessages = useCallback2(() => setMessages([]), []);
3621
- const commandContextRef = useRef({
3679
+ const commandContextRef = useRef2({
3622
3680
  messages,
3623
3681
  model,
3624
3682
  cwd,
@@ -3644,20 +3702,19 @@ function REPL({ initialPrompt }) {
3644
3702
  content: userMessage,
3645
3703
  timestamp: Date.now()
3646
3704
  };
3705
+ freeze({ id: userMsg.id, kind: "user", content: userMessage });
3647
3706
  setMessages((prev) => [...prev, userMsg]);
3648
3707
  setIsStreaming(true);
3649
- streamBufferRef.current = "";
3650
- setStreamingText("");
3651
- setToolResults([]);
3708
+ setReceivedText(false);
3709
+ setActiveTool(null);
3652
3710
  setCommandOutput(null);
3653
- setMascotMood("thinking");
3711
+ streamingRef.current?.clear();
3654
3712
  const controller = new AbortController();
3655
3713
  abortRef.current = controller;
3656
3714
  const allMessages = [...messagesRef.current, userMsg];
3657
3715
  const currentModel = modelRef.current;
3658
3716
  const currentSystemPrompt = systemPromptRef.current;
3659
- let receivedFirstText = false;
3660
- startStreamFlush();
3717
+ let gotFirstText = false;
3661
3718
  try {
3662
3719
  for await (const event of query({
3663
3720
  model: currentModel,
@@ -3670,24 +3727,18 @@ function REPL({ initialPrompt }) {
3670
3727
  if (controller.signal.aborted) break;
3671
3728
  switch (event.type) {
3672
3729
  case "text_delta":
3673
- streamBufferRef.current += event.text;
3674
- if (!receivedFirstText) {
3675
- receivedFirstText = true;
3676
- setMascotMood("idle");
3730
+ streamingRef.current?.append(event.text);
3731
+ if (!gotFirstText) {
3732
+ gotFirstText = true;
3733
+ setReceivedText(true);
3677
3734
  }
3678
3735
  break;
3679
3736
  case "tool_call":
3680
- stopStreamFlush();
3681
3737
  setActiveTool({ name: event.toolCall.function.name, args: event.toolCall.function.arguments });
3682
- setMascotMood("working");
3683
3738
  break;
3684
3739
  case "tool_result":
3685
3740
  setActiveTool(null);
3686
- setMascotMood(event.isError ? "error" : "working");
3687
- setToolResults((prev) => [
3688
- ...prev,
3689
- { id: nanoid6(), toolName: event.toolName, output: event.output, isError: event.isError }
3690
- ]);
3741
+ freeze({ id: nanoid6(), kind: "tool-result", toolName: event.toolName, output: event.output, isError: event.isError });
3691
3742
  break;
3692
3743
  case "usage":
3693
3744
  setTokenCounts((prev) => ({
@@ -3696,21 +3747,22 @@ function REPL({ initialPrompt }) {
3696
3747
  }));
3697
3748
  break;
3698
3749
  case "assistant_message":
3699
- stopStreamFlush();
3750
+ streamingRef.current?.clear();
3751
+ setReceivedText(false);
3752
+ gotFirstText = false;
3753
+ if (event.message.content) {
3754
+ freeze({ id: event.message.id, kind: "assistant", content: event.message.content });
3755
+ }
3700
3756
  setMessages((prev) => {
3701
3757
  const updated = [...prev, event.message];
3702
3758
  saveSession(sessionId.current, updated, currentModel, cwd).catch(() => {
3703
3759
  });
3704
3760
  return updated;
3705
3761
  });
3706
- streamBufferRef.current = "";
3707
- setStreamingText("");
3708
- receivedFirstText = false;
3709
- startStreamFlush();
3710
3762
  break;
3711
3763
  case "error":
3712
- stopStreamFlush();
3713
- setMascotMood("error");
3764
+ streamingRef.current?.clear();
3765
+ freeze({ id: nanoid6(), kind: "assistant", content: `Error: ${event.error}` });
3714
3766
  setMessages((prev) => [
3715
3767
  ...prev,
3716
3768
  { id: nanoid6(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
@@ -3721,24 +3773,21 @@ function REPL({ initialPrompt }) {
3721
3773
  } catch (err) {
3722
3774
  if (err.name !== "AbortError") {
3723
3775
  const msg = err instanceof Error ? err.message : String(err);
3776
+ freeze({ id: nanoid6(), kind: "assistant", content: `Error: ${msg}` });
3724
3777
  setMessages((prev) => [
3725
3778
  ...prev,
3726
3779
  { id: nanoid6(), role: "assistant", content: `Error: ${msg}`, timestamp: Date.now() }
3727
3780
  ]);
3728
3781
  }
3729
3782
  } finally {
3730
- stopStreamFlush();
3731
- streamBufferRef.current = "";
3783
+ streamingRef.current?.clear();
3732
3784
  setIsStreaming(false);
3733
- setStreamingText("");
3785
+ setReceivedText(false);
3734
3786
  setActiveTool(null);
3735
- setToolResults([]);
3736
- setMascotMood((prev) => prev === "error" ? "error" : "success");
3737
- setTimeout(() => setMascotMood("idle"), 2e3);
3738
3787
  abortRef.current = null;
3739
3788
  }
3740
3789
  },
3741
- [tools, cwd, startStreamFlush, stopStreamFlush]
3790
+ [tools, cwd, freeze]
3742
3791
  );
3743
3792
  const handleSubmit = useCallback2(
3744
3793
  async (value) => {
@@ -3756,17 +3805,13 @@ function REPL({ initialPrompt }) {
3756
3805
  if (result.replaceMessages) {
3757
3806
  setMessages(result.replaceMessages);
3758
3807
  }
3759
- if (result.foxMood) {
3760
- setMascotMood(result.foxMood);
3761
- setTimeout(() => setMascotMood("idle"), 3e3);
3762
- }
3763
3808
  if (result.exit) return;
3764
3809
  if (!result.silent && result.output) {
3765
3810
  runQuery(result.output);
3766
3811
  return;
3767
3812
  }
3768
3813
  if (result.output) {
3769
- setCommandOutput(result.output);
3814
+ freeze({ id: nanoid6(), kind: "info", text: result.output });
3770
3815
  }
3771
3816
  }
3772
3817
  return;
@@ -3775,16 +3820,15 @@ function REPL({ initialPrompt }) {
3775
3820
  const cmd = value.slice(1).trim();
3776
3821
  if (!cmd) return;
3777
3822
  setIsStreaming(true);
3778
- setCommandOutput(null);
3779
3823
  try {
3780
3824
  const result = await BashTool.call(
3781
3825
  { command: cmd },
3782
3826
  { cwd, abortSignal: new AbortController().signal }
3783
3827
  );
3784
- setCommandOutput(result.output);
3828
+ freeze({ id: nanoid6(), kind: "info", text: result.output });
3785
3829
  } catch (err) {
3786
3830
  const msg = err instanceof Error ? err.message : String(err);
3787
- setCommandOutput(`Error: ${msg}`);
3831
+ freeze({ id: nanoid6(), kind: "info", text: `Error: ${msg}` });
3788
3832
  } finally {
3789
3833
  setIsStreaming(false);
3790
3834
  }
@@ -3792,16 +3836,16 @@ function REPL({ initialPrompt }) {
3792
3836
  }
3793
3837
  runQuery(value);
3794
3838
  },
3795
- [addToHistory, exit, cwd, runQuery]
3839
+ [addToHistory, exit, cwd, runQuery, freeze]
3796
3840
  );
3797
- useEffect(() => {
3841
+ useEffect2(() => {
3798
3842
  if (initialPrompt && !hasRun.current) {
3799
3843
  hasRun.current = true;
3800
3844
  runQuery(initialPrompt);
3801
3845
  }
3802
3846
  }, [initialPrompt, runQuery]);
3803
- const autoCompactRef = useRef(false);
3804
- useEffect(() => {
3847
+ const autoCompactRef = useRef2(false);
3848
+ useEffect2(() => {
3805
3849
  if (isStreaming || autoCompactRef.current || messages.length < 6) return;
3806
3850
  const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0);
3807
3851
  const estTokens = Math.ceil(totalChars / 4) + 2e3;
@@ -3809,7 +3853,7 @@ function REPL({ initialPrompt }) {
3809
3853
  const usage = estTokens / limit;
3810
3854
  if (usage > 0.85) {
3811
3855
  autoCompactRef.current = true;
3812
- setCommandOutput("\x1B[33m[!] Context 85%+ full \u2014 auto-compacting...\x1B[0m");
3856
+ freeze({ id: nanoid6(), kind: "info", text: "\x1B[33m[!] Context 85%+ full \u2014 auto-compacting...\x1B[0m" });
3813
3857
  const transcript = messages.filter((m) => m.role === "user" || m.role === "assistant" && m.content).map((m) => `${m.role}: ${m.content}`).join("\n\n");
3814
3858
  getProvider().chat({
3815
3859
  model,
@@ -3827,10 +3871,10 @@ ${result.content}
3827
3871
  };
3828
3872
  setMessages([summaryMsg]);
3829
3873
  setTokenCounts({ input: Math.ceil(result.content.length / 4), output: 0 });
3830
- setCommandOutput("\x1B[32m[ok] Auto-compacted conversation.\x1B[0m");
3874
+ freeze({ id: nanoid6(), kind: "info", text: "\x1B[32m[ok] Auto-compacted conversation.\x1B[0m" });
3831
3875
  autoCompactRef.current = false;
3832
3876
  }).catch(() => {
3833
- setCommandOutput("\x1B[31m[err] Auto-compact failed.\x1B[0m");
3877
+ freeze({ id: nanoid6(), kind: "info", text: "\x1B[31m[err] Auto-compact failed.\x1B[0m" });
3834
3878
  autoCompactRef.current = false;
3835
3879
  });
3836
3880
  }
@@ -3844,39 +3888,35 @@ ${result.content}
3844
3888
  }
3845
3889
  }
3846
3890
  });
3847
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
3848
- /* @__PURE__ */ jsx7(Banner, { model, cwd, providerName, providerOnline, mood: mascotMood }),
3849
- /* @__PURE__ */ jsx7(Messages, { messages }),
3850
- commandOutput ? /* @__PURE__ */ jsx7(Box6, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx7(Text6, { children: commandOutput }) }) : null,
3851
- toolResults.map((tr) => /* @__PURE__ */ jsx7(ToolResultDisplay, { toolName: tr.toolName, output: tr.output, isError: tr.isError }, tr.id)),
3852
- activeTool ? /* @__PURE__ */ jsx7(ActiveToolCall, { toolName: activeTool.name, args: activeTool.args }) : null,
3853
- isStreaming && streamingText ? /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, children: [
3854
- /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "\u23BF " }),
3855
- /* @__PURE__ */ jsx7(Text6, { color: theme.text, wrap: "wrap", children: streamingText })
3856
- ] }) : null,
3857
- isStreaming && !streamingText && !activeTool ? /* @__PURE__ */ jsxs6(Box6, { marginLeft: 2, children: [
3858
- /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "..." }),
3859
- /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: " Thinking" })
3860
- ] }) : null,
3861
- /* @__PURE__ */ jsx7(
3862
- UserInput,
3863
- {
3864
- value: inputValue,
3865
- onChange: setInputValue,
3866
- onSubmit: handleSubmit,
3867
- disabled: isStreaming,
3868
- history: inputHistory
3869
- }
3870
- ),
3871
- /* @__PURE__ */ jsx7(
3872
- StatusLine,
3873
- {
3874
- model,
3875
- messageCount: messages.length,
3876
- tokenEstimate: tokenCounts.input + tokenCounts.output,
3877
- isStreaming
3878
- }
3879
- )
3891
+ return /* @__PURE__ */ jsxs6(Fragment3, { children: [
3892
+ /* @__PURE__ */ jsx7(Static, { items: frozen, children: (item) => /* @__PURE__ */ jsx7(FrozenItemView, { item }, item.id) }),
3893
+ /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [
3894
+ activeTool ? /* @__PURE__ */ jsx7(ActiveToolCall, { toolName: activeTool.name, args: activeTool.args }) : null,
3895
+ /* @__PURE__ */ jsx7(StreamingDisplay, { ref: streamingRef }),
3896
+ isStreaming && !receivedText && !activeTool ? /* @__PURE__ */ jsxs6(Box6, { marginLeft: 2, children: [
3897
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "..." }),
3898
+ /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: " Thinking" })
3899
+ ] }) : null,
3900
+ /* @__PURE__ */ jsx7(
3901
+ UserInput,
3902
+ {
3903
+ value: inputValue,
3904
+ onChange: setInputValue,
3905
+ onSubmit: handleSubmit,
3906
+ disabled: isStreaming,
3907
+ history: inputHistory
3908
+ }
3909
+ ),
3910
+ /* @__PURE__ */ jsx7(
3911
+ StatusLine,
3912
+ {
3913
+ model,
3914
+ messageCount: messages.length,
3915
+ tokenEstimate: tokenCounts.input + tokenCounts.output,
3916
+ isStreaming
3917
+ }
3918
+ )
3919
+ ] })
3880
3920
  ] });
3881
3921
  }
3882
3922
 
@@ -3884,7 +3924,7 @@ ${result.content}
3884
3924
  init_providers();
3885
3925
  import { jsx as jsx8 } from "react/jsx-runtime";
3886
3926
  var program = new Command();
3887
- program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.4.1").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("-c, --continue", "Resume the most recent session").option("--resume <id>", "Resume a specific session by ID").option("--max-turns <n>", "Maximum tool-use turns per query", "30").option("--debug", "Enable debug logging to stderr").option("--output-format <format>", "Output format for non-interactive mode (text, json)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
3927
+ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.4.3").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("-c, --continue", "Resume the most recent session").option("--resume <id>", "Resume a specific session by ID").option("--max-turns <n>", "Maximum tool-use turns per query", "30").option("--debug", "Enable debug logging to stderr").option("--output-format <format>", "Output format for non-interactive mode (text, json)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
3888
3928
  const { model, prompt, provider, systemPrompt } = options;
3889
3929
  if (options.debug) {
3890
3930
  const { setDebugMode: setDebugMode2 } = await Promise.resolve().then(() => (init_debug(), debug_exports));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkfoo-code",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Darkfoo Code — local AI coding assistant powered by Ollama, vLLM, llama.cpp, and other LLM providers",
5
5
  "type": "module",
6
6
  "license": "MIT",