hoomanjs 1.31.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -11
- package/dist/chat/app.d.ts +3 -1
- package/dist/chat/app.js +191 -87
- package/dist/chat/app.js.map +1 -1
- package/dist/chat/components/BottomChrome.d.ts +2 -1
- package/dist/chat/components/BottomChrome.js +2 -2
- package/dist/chat/components/BottomChrome.js.map +1 -1
- package/dist/chat/components/ChatMessage.js +1 -1
- package/dist/chat/components/ChatMessage.js.map +1 -1
- package/dist/chat/components/StatusBar.d.ts +2 -1
- package/dist/chat/components/StatusBar.js +2 -2
- package/dist/chat/components/StatusBar.js.map +1 -1
- package/dist/chat/components/ThoughtEvent.js +1 -1
- package/dist/chat/components/ThoughtEvent.js.map +1 -1
- package/dist/chat/components/ToolEvent.js +1 -1
- package/dist/chat/components/ToolEvent.js.map +1 -1
- package/dist/chat/components/ToolEventFileResult.js +1 -1
- package/dist/chat/components/ToolEventFileResult.js.map +1 -1
- package/dist/chat/components/Transcript.d.ts +13 -10
- package/dist/chat/components/Transcript.js +15 -76
- package/dist/chat/components/Transcript.js.map +1 -1
- package/dist/chat/components/markdown/BlockRenderer.d.ts +2 -2
- package/dist/chat/index.d.ts +5 -1
- package/dist/chat/index.js +21 -19
- package/dist/chat/index.js.map +1 -1
- package/dist/cli.js +58 -38
- package/dist/cli.js.map +1 -1
- package/dist/configure/app.js +4 -5
- package/dist/configure/app.js.map +1 -1
- package/dist/configure/index.js +9 -0
- package/dist/configure/index.js.map +1 -1
- package/dist/core/agent/index.d.ts +3 -1
- package/dist/core/agent/index.js +21 -4
- package/dist/core/agent/index.js.map +1 -1
- package/dist/core/prompts/system.d.ts +4 -3
- package/dist/core/prompts/system.js +10 -9
- package/dist/core/prompts/system.js.map +1 -1
- package/dist/core/skills/built-in/hooman-config/SKILL.md +1 -1
- package/package.json +1 -1
- package/dist/configure/components/HomeScreen.d.ts +0 -12
- package/dist/configure/components/HomeScreen.js +0 -7
- package/dist/configure/components/HomeScreen.js.map +0 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ It gives you a practical toolkit to build and run agent workflows:
|
|
|
21
21
|
- a one-shot `exec` command for single prompts
|
|
22
22
|
- a stateful `chat` interface for iterative sessions
|
|
23
23
|
- a `daemon` command for channel-driven MCP automation
|
|
24
|
-
- an
|
|
24
|
+
- an in-chat `/config` workflow (Ink-powered) for app config, prompts, MCP servers, and installed skills
|
|
25
25
|
- an `acp` command for running Hooman as an Agent Client Protocol (ACP) agent over stdio
|
|
26
26
|
|
|
27
27
|
## Related
|
|
@@ -53,7 +53,6 @@ It gives you a practical toolkit to build and run agent workflows:
|
|
|
53
53
|
Fastest way to get started without cloning the repo:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
npx hoomanjs configure
|
|
57
56
|
npx hoomanjs
|
|
58
57
|
|
|
59
58
|
# or install globally
|
|
@@ -63,14 +62,13 @@ npm i -g hoomanjs
|
|
|
63
62
|
Or with Bun:
|
|
64
63
|
|
|
65
64
|
```bash
|
|
66
|
-
bunx hoomanjs configure
|
|
67
65
|
bunx hoomanjs
|
|
68
66
|
```
|
|
69
67
|
|
|
70
68
|
Recommended first run:
|
|
71
69
|
|
|
72
|
-
1.
|
|
73
|
-
2.
|
|
70
|
+
1. Start chatting with `hooman` (same as `hooman chat`).
|
|
71
|
+
2. Run `/config` in chat to choose your LLM provider and model, and to manage MCP servers and skills.
|
|
74
72
|
3. Use `hooman exec "your prompt"` for one-off tasks.
|
|
75
73
|
|
|
76
74
|
## Must have
|
|
@@ -182,6 +180,18 @@ Start in ask mode:
|
|
|
182
180
|
hooman chat --mode ask
|
|
183
181
|
```
|
|
184
182
|
|
|
183
|
+
### Chat commands
|
|
184
|
+
|
|
185
|
+
Inside an interactive `chat` session, type `/` to discover slash commands:
|
|
186
|
+
|
|
187
|
+
- `/model` - pick or set the chat model for this session.
|
|
188
|
+
- `/mode` - switch the session mode (`agent`, `ask`, `plan`); see [Session mode](#session-mode).
|
|
189
|
+
- `/yolo` - toggle auto-approve of tool calls (`on` / `off`).
|
|
190
|
+
- `/init` - generate or refresh `AGENTS.md` for the current project.
|
|
191
|
+
- `/compact` - compact the conversation history now and persist the result.
|
|
192
|
+
- `/new` - start a fresh chat session.
|
|
193
|
+
- `/config` - launch the configuration workflow (see below).
|
|
194
|
+
|
|
185
195
|
### Session mode
|
|
186
196
|
|
|
187
197
|
`exec`, `chat`, and `daemon` accept **`-m` / `--mode`** with:
|
|
@@ -243,15 +253,15 @@ Runtime tool and prompt switches are controlled from `config.json`:
|
|
|
243
253
|
- `tools.agents.enabled` (enables built-in `run_subagents` tool)
|
|
244
254
|
- `tools.agents.concurrency` (defaults to `3` when omitted on load; a freshly generated default `config.json` uses `2`)
|
|
245
255
|
|
|
246
|
-
### `
|
|
256
|
+
### `/config`
|
|
247
257
|
|
|
248
|
-
|
|
258
|
+
The configuration workflow is launched from inside a `chat` session with the `/config` slash command (there is no separate top-level `configure` command). It takes over the terminal on the alternate screen buffer while open, and restores the chat session on exit. Any config changes are picked up when the session re-bootstraps.
|
|
249
259
|
|
|
250
|
-
```
|
|
251
|
-
|
|
260
|
+
```text
|
|
261
|
+
/config
|
|
252
262
|
```
|
|
253
263
|
|
|
254
|
-
The
|
|
264
|
+
The configuration UI currently lets you:
|
|
255
265
|
|
|
256
266
|
- edit app configuration values
|
|
257
267
|
- choose search provider and set its API key
|
|
@@ -751,7 +761,7 @@ The local skills folder is treated as a parent directory of skill subdirectories
|
|
|
751
761
|
|
|
752
762
|
When a session starts, the plugin injects available skill metadata into the system prompt and exposes the `skills` tool so the model can activate a skill and load its full instructions on demand.
|
|
753
763
|
|
|
754
|
-
The
|
|
764
|
+
The `/config` workflow can:
|
|
755
765
|
|
|
756
766
|
- search the public skills catalog
|
|
757
767
|
- install a skill from a source string, repo, URL, or local path
|
package/dist/chat/app.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ type ChatAppProps = {
|
|
|
15
15
|
steering: ChatTurnSteeringController;
|
|
16
16
|
prompt?: string;
|
|
17
17
|
onExit: () => void;
|
|
18
|
+
onNewSession: () => void;
|
|
19
|
+
onConfigure: () => void;
|
|
18
20
|
};
|
|
19
|
-
export declare function ChatApp({ agent, config, sessionId, manager, registry, approvals, steering, prompt, onExit, }: ChatAppProps): React.JSX.Element;
|
|
21
|
+
export declare function ChatApp({ agent, config, sessionId, manager, registry, approvals, steering, prompt, onExit, onNewSession, onConfigure, }: ChatAppProps): React.JSX.Element;
|
|
20
22
|
export {};
|
package/dist/chat/app.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import fastq from "fastq";
|
|
4
|
-
import { Box, useApp, useInput, useWindowSize } from "ink";
|
|
4
|
+
import { Box, Static, useApp, useInput, useWindowSize } from "ink";
|
|
5
5
|
import { Message, TextBlock, ToolResultBlock, ToolUseBlock, } from "@strands-agents/sdk";
|
|
6
6
|
import { accumulateUsage, createEmptyUsage, } from "../core/utils/strands-usage-accumulate.js";
|
|
7
7
|
import { modelProviders } from "../core/models/index.js";
|
|
8
8
|
import { formatModeNames, isKnownSessionMode } from "../core/modes/index.js";
|
|
9
9
|
import { takeFileToolDisplay } from "../core/state/file-tool-display.js";
|
|
10
10
|
import { BottomChrome } from "./components/BottomChrome.js";
|
|
11
|
-
import {
|
|
11
|
+
import { LiveTranscript, TranscriptLine } from "./components/Transcript.js";
|
|
12
|
+
import { EmptyChatBanner } from "./components/EmptyChatBanner.js";
|
|
12
13
|
import { getTodoViewState } from "../core/state/todos.js";
|
|
13
14
|
import { isExitRequested } from "../core/state/exit-request.js";
|
|
14
15
|
import { getModeState, setSessionMode, } from "../core/state/session-mode.js";
|
|
15
16
|
import { isYoloEnabled, setYoloEnabled } from "../core/state/yolo.js";
|
|
16
17
|
import { applySessionMode } from "../core/agent/sync-tool-registry-mode.js";
|
|
18
|
+
import { getAgentConversationManager, getAgentSessionManager, } from "../core/agent/index.js";
|
|
17
19
|
import { attachmentPathsToPromptBlocks } from "../core/utils/attachments.js";
|
|
18
20
|
import { isMouseInput } from "./mouse.js";
|
|
19
21
|
import { readBundledPrompt } from "../core/prompts/bundled.js";
|
|
@@ -237,6 +239,14 @@ function listModelsText(config) {
|
|
|
237
239
|
].join("\n");
|
|
238
240
|
}
|
|
239
241
|
const SLASH_COMMANDS = [
|
|
242
|
+
{
|
|
243
|
+
name: "compact",
|
|
244
|
+
description: "Compact conversation history now.",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "config",
|
|
248
|
+
description: "Launch the configuration flow.",
|
|
249
|
+
},
|
|
240
250
|
{
|
|
241
251
|
name: "init",
|
|
242
252
|
description: "Generate or refresh AGENTS.md for this project.",
|
|
@@ -249,6 +259,10 @@ const SLASH_COMMANDS = [
|
|
|
249
259
|
name: "model",
|
|
250
260
|
description: "Pick or set the chat model.",
|
|
251
261
|
},
|
|
262
|
+
{
|
|
263
|
+
name: "new",
|
|
264
|
+
description: "Start a new chat session.",
|
|
265
|
+
},
|
|
252
266
|
{
|
|
253
267
|
name: "yolo",
|
|
254
268
|
description: "Auto-approve tools (on|off).",
|
|
@@ -268,36 +282,7 @@ function matchingSlashCommands(input) {
|
|
|
268
282
|
}
|
|
269
283
|
return SLASH_COMMANDS.filter((item) => item.name.startsWith(query));
|
|
270
284
|
}
|
|
271
|
-
function
|
|
272
|
-
let rows = 8;
|
|
273
|
-
if (params.running && params.todoCount > 0) {
|
|
274
|
-
rows += 2 + params.todoCount;
|
|
275
|
-
}
|
|
276
|
-
if (params.queueCount > 0) {
|
|
277
|
-
rows += 2 + params.queueCount;
|
|
278
|
-
}
|
|
279
|
-
if (params.pendingApproval) {
|
|
280
|
-
rows += 4;
|
|
281
|
-
return rows;
|
|
282
|
-
}
|
|
283
|
-
if (params.picker === "model") {
|
|
284
|
-
rows += 2 + 4;
|
|
285
|
-
return rows;
|
|
286
|
-
}
|
|
287
|
-
if (params.picker === "yolo") {
|
|
288
|
-
rows += 2 + 2;
|
|
289
|
-
return rows;
|
|
290
|
-
}
|
|
291
|
-
if (params.picker === "mode") {
|
|
292
|
-
rows += 2 + 3;
|
|
293
|
-
return rows;
|
|
294
|
-
}
|
|
295
|
-
if (params.slashCommandCount > 0) {
|
|
296
|
-
rows += params.slashCommandCount + 1;
|
|
297
|
-
}
|
|
298
|
-
return rows;
|
|
299
|
-
}
|
|
300
|
-
export function ChatApp({ agent, config, sessionId, manager, registry, approvals, steering, prompt, onExit, }) {
|
|
285
|
+
export function ChatApp({ agent, config, sessionId, manager, registry, approvals, steering, prompt, onExit, onNewSession, onConfigure, }) {
|
|
301
286
|
const { exit } = useApp();
|
|
302
287
|
const windowSize = useWindowSize();
|
|
303
288
|
const [input, setInput] = useState("");
|
|
@@ -318,7 +303,7 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
318
303
|
}, []);
|
|
319
304
|
const [slashHighlightIndex, setSlashHighlightIndex] = useState(0);
|
|
320
305
|
const [queuedPrompts, setQueuedPrompts] = useState([]);
|
|
321
|
-
const [
|
|
306
|
+
const [mcpNeedsAttention, setMcpNeedsAttention] = useState(false);
|
|
322
307
|
const [todoState, setTodoState] = useState(() => getTodoViewState(agent));
|
|
323
308
|
const mountedRef = useRef(true);
|
|
324
309
|
const runningRef = useRef(false);
|
|
@@ -354,6 +339,27 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
354
339
|
cancelled = true;
|
|
355
340
|
};
|
|
356
341
|
}, [registry]);
|
|
342
|
+
// Surface MCP OAuth state in the status bar. Re-checked whenever a turn ends,
|
|
343
|
+
// since a tool call may have triggered (or completed) an auth flow mid-session.
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
let cancelled = false;
|
|
346
|
+
void manager
|
|
347
|
+
.listAuthStatuses()
|
|
348
|
+
.then((rows) => {
|
|
349
|
+
if (cancelled) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
setMcpNeedsAttention(rows.some((row) => row.status === "unauthenticated" || row.status === "expired"));
|
|
353
|
+
})
|
|
354
|
+
.catch(() => {
|
|
355
|
+
if (!cancelled) {
|
|
356
|
+
setMcpNeedsAttention(false);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return () => {
|
|
360
|
+
cancelled = true;
|
|
361
|
+
};
|
|
362
|
+
}, [manager, running]);
|
|
357
363
|
useEffect(() => {
|
|
358
364
|
if (!running || turnStartedAt === null) {
|
|
359
365
|
setTurnElapsedMs(0);
|
|
@@ -427,6 +433,9 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
427
433
|
const updateLine = useCallback((id, patch) => {
|
|
428
434
|
setLines((prev) => prev.map((line) => (line.id === id ? { ...line, ...patch } : line)));
|
|
429
435
|
}, []);
|
|
436
|
+
const removeLine = useCallback((id) => {
|
|
437
|
+
setLines((prev) => prev.filter((line) => line.id !== id));
|
|
438
|
+
}, []);
|
|
430
439
|
const moveLineToEnd = useCallback((id) => {
|
|
431
440
|
setLines((prev) => {
|
|
432
441
|
const index = prev.findIndex((line) => line.id === id);
|
|
@@ -463,11 +472,17 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
463
472
|
if (!id) {
|
|
464
473
|
return;
|
|
465
474
|
}
|
|
466
|
-
|
|
475
|
+
const text = `${assistantCommittedTextRef.current}${streamedAssistantBlockRef.current ?? ""}`;
|
|
476
|
+
if (text.trim().length === 0) {
|
|
477
|
+
removeLine(id);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
updateLine(id, { done: true });
|
|
481
|
+
}
|
|
467
482
|
assistantLineIdRef.current = null;
|
|
468
483
|
assistantCommittedTextRef.current = "";
|
|
469
484
|
streamedAssistantBlockRef.current = null;
|
|
470
|
-
}, [updateLine]);
|
|
485
|
+
}, [removeLine, updateLine]);
|
|
471
486
|
const ensureAssistantLine = useCallback(() => {
|
|
472
487
|
const existing = assistantLineIdRef.current;
|
|
473
488
|
if (existing) {
|
|
@@ -652,6 +667,95 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
652
667
|
applySessionMode(agent);
|
|
653
668
|
bumpSessionChrome();
|
|
654
669
|
}, [agent, appendLine, bumpSessionChrome]);
|
|
670
|
+
const handleCompactCommand = useCallback(async () => {
|
|
671
|
+
if (runningRef.current) {
|
|
672
|
+
appendLine({
|
|
673
|
+
id: nowId(),
|
|
674
|
+
role: "system",
|
|
675
|
+
title: "compact",
|
|
676
|
+
content: "Wait for the active turn to finish before compacting history.",
|
|
677
|
+
done: true,
|
|
678
|
+
});
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const conversationManager = getAgentConversationManager(agent);
|
|
682
|
+
if (!conversationManager) {
|
|
683
|
+
appendLine({
|
|
684
|
+
id: nowId(),
|
|
685
|
+
role: "system",
|
|
686
|
+
title: "compact",
|
|
687
|
+
content: "This session does not have a conversation manager to compact.",
|
|
688
|
+
done: true,
|
|
689
|
+
});
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const beforeCount = agent.messages.length;
|
|
693
|
+
try {
|
|
694
|
+
const reduced = await conversationManager.reduce({
|
|
695
|
+
agent,
|
|
696
|
+
model: agent.model,
|
|
697
|
+
});
|
|
698
|
+
const afterCount = agent.messages.length;
|
|
699
|
+
if (!reduced) {
|
|
700
|
+
appendLine({
|
|
701
|
+
id: nowId(),
|
|
702
|
+
role: "system",
|
|
703
|
+
title: "compact",
|
|
704
|
+
content: "Conversation history is already too short to compact.",
|
|
705
|
+
done: true,
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
await getAgentSessionManager(agent)?.saveSnapshot({
|
|
710
|
+
target: agent,
|
|
711
|
+
isLatest: true,
|
|
712
|
+
});
|
|
713
|
+
appendLine({
|
|
714
|
+
id: nowId(),
|
|
715
|
+
role: "system",
|
|
716
|
+
title: "compact",
|
|
717
|
+
content: `Compacted conversation history for future turns (${beforeCount} messages -> ${afterCount}).`,
|
|
718
|
+
done: true,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
appendLine({
|
|
723
|
+
id: nowId(),
|
|
724
|
+
role: "system",
|
|
725
|
+
title: "compact",
|
|
726
|
+
content: error instanceof Error ? error.message : String(error),
|
|
727
|
+
done: true,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}, [agent, appendLine]);
|
|
731
|
+
const handleNewCommand = useCallback(() => {
|
|
732
|
+
if (runningRef.current) {
|
|
733
|
+
appendLine({
|
|
734
|
+
id: nowId(),
|
|
735
|
+
role: "system",
|
|
736
|
+
title: "new",
|
|
737
|
+
content: "Wait for the active turn to finish before starting a new session.",
|
|
738
|
+
done: true,
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
onNewSession();
|
|
743
|
+
exit();
|
|
744
|
+
}, [appendLine, exit, onNewSession]);
|
|
745
|
+
const handleConfigCommand = useCallback(() => {
|
|
746
|
+
if (runningRef.current) {
|
|
747
|
+
appendLine({
|
|
748
|
+
id: nowId(),
|
|
749
|
+
role: "system",
|
|
750
|
+
title: "config",
|
|
751
|
+
content: "Wait for the active turn to finish before launching configuration.",
|
|
752
|
+
done: true,
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
onConfigure();
|
|
757
|
+
exit();
|
|
758
|
+
}, [appendLine, exit, onConfigure]);
|
|
655
759
|
const runTurn = useCallback(async (prompt) => {
|
|
656
760
|
const trimmed = prompt.text.trim();
|
|
657
761
|
if (!trimmed && prompt.attachments.length === 0) {
|
|
@@ -895,7 +999,6 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
895
999
|
id: nowId(),
|
|
896
1000
|
prompt: { ...normalized, text: trimmed },
|
|
897
1001
|
};
|
|
898
|
-
setTranscriptScrollOffset(0);
|
|
899
1002
|
setQueuedPrompts((prev) => [...prev, item]);
|
|
900
1003
|
void queueRef.current?.push(item).catch((error) => {
|
|
901
1004
|
if (!mountedRef.current) {
|
|
@@ -968,6 +1071,21 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
968
1071
|
setInput("");
|
|
969
1072
|
return;
|
|
970
1073
|
}
|
|
1074
|
+
if (command.name === "compact") {
|
|
1075
|
+
void handleCompactCommand();
|
|
1076
|
+
setInput("");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (command.name === "config") {
|
|
1080
|
+
handleConfigCommand();
|
|
1081
|
+
setInput("");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (command.name === "new") {
|
|
1085
|
+
handleNewCommand();
|
|
1086
|
+
setInput("");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
971
1089
|
}
|
|
972
1090
|
if (pushPrompt(value)) {
|
|
973
1091
|
setInput("");
|
|
@@ -975,7 +1093,10 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
975
1093
|
}, [
|
|
976
1094
|
appendLine,
|
|
977
1095
|
handleModelCommand,
|
|
1096
|
+
handleCompactCommand,
|
|
1097
|
+
handleConfigCommand,
|
|
978
1098
|
handleModeCommand,
|
|
1099
|
+
handleNewCommand,
|
|
979
1100
|
handleYoloCommand,
|
|
980
1101
|
pendingApproval,
|
|
981
1102
|
pushPrompt,
|
|
@@ -985,24 +1106,9 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
985
1106
|
if (isMouseInput(inputKey)) {
|
|
986
1107
|
return;
|
|
987
1108
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (key.pageDown) {
|
|
994
|
-
setTranscriptScrollOffset((offset) => Math.max(0, offset - transcriptScrollStep));
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
if (key.home) {
|
|
998
|
-
setTranscriptScrollOffset(Math.max(0, lines.length - 1));
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
if (key.end) {
|
|
1002
|
-
setTranscriptScrollOffset(0);
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1109
|
+
// Scrolling is handled natively by the terminal: finished transcript
|
|
1110
|
+
// lines are flushed to real scrollback via <Static>, so the user scrolls
|
|
1111
|
+
// with their mouse/trackpad/terminal like any other command output.
|
|
1006
1112
|
if (key.ctrl && inputKey.toLowerCase() === "c") {
|
|
1007
1113
|
if (runningRef.current) {
|
|
1008
1114
|
agent.cancel();
|
|
@@ -1041,38 +1147,36 @@ export function ChatApp({ agent, config, sessionId, manager, registry, approvals
|
|
|
1041
1147
|
return `${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
|
1042
1148
|
}, [turnElapsedMs]);
|
|
1043
1149
|
const inputHint = running && queuedPrompts.length > 0 ? INPUT_HINT_WITH_STEERING : INPUT_HINT;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
void handleModeCommand(value);
|
|
1076
|
-
}, onInputChange: setInput, onSubmit: onSubmit })] }));
|
|
1150
|
+
// Split the transcript at the first not-yet-finalized entry. Everything before
|
|
1151
|
+
// it is final and append-only, so it can be flushed to the terminal scrollback
|
|
1152
|
+
// via <Static> (printed once, never re-rendered). Everything from there on is
|
|
1153
|
+
// the live tail that Ink keeps re-rendering: streaming text, running tools,
|
|
1154
|
+
// active reasoning. Using the done-prefix (rather than per-line `done`)
|
|
1155
|
+
// guarantees Static only ever grows and stays in chronological order even when
|
|
1156
|
+
// tools finish out of order.
|
|
1157
|
+
const committedCount = useMemo(() => {
|
|
1158
|
+
let end = 0;
|
|
1159
|
+
while (end < lines.length && lines[end]?.done) {
|
|
1160
|
+
end += 1;
|
|
1161
|
+
}
|
|
1162
|
+
return end;
|
|
1163
|
+
}, [lines]);
|
|
1164
|
+
const committedLines = useMemo(() => lines.slice(0, committedCount), [lines, committedCount]);
|
|
1165
|
+
const liveLines = useMemo(() => lines.slice(committedCount), [lines, committedCount]);
|
|
1166
|
+
// Before the first prompt there is nothing in scrollback yet, so grow the live
|
|
1167
|
+
// region to the full viewport and vertically center the banner (with the
|
|
1168
|
+
// composer pinned to the bottom). Once any line exists the region collapses to
|
|
1169
|
+
// its natural height and finished lines flow into the terminal scrollback.
|
|
1170
|
+
const isEmpty = lines.length === 0;
|
|
1171
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Static, { items: committedLines, children: (line) => (_jsx(Box, { paddingX: 1, children: _jsx(TranscriptLine, { line: line, assistantName: config.name }) }, line.id)) }), _jsxs(Box, { flexDirection: "column", width: "100%", paddingX: 1, ...(isEmpty ? { height: Math.max(1, windowSize.rows - 1) } : {}), children: [isEmpty ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "center", children: _jsx(EmptyChatBanner, {}) })) : null, _jsx(LiveTranscript, { lines: liveLines, assistantName: config.name }), _jsx(BottomChrome, { config: config, running: running, status: status, sessionId: sessionId, currentModel: currentModelLabel(config), yoloOn: isYoloEnabled(agent), sessionMode: getModeState(agent).mode, elapsedLabel: elapsedLabel, turnCount: turnCount, totalTools: totalTools, skillsFound: skillsFound, manager: manager, mcpNeedsAttention: mcpNeedsAttention, usage: usage, todoState: todoState, queuedPrompts: queuedPrompts, pendingApproval: Boolean(pendingApproval), picker: picker, slashCommands: slashCommands, slashHighlightIndex: slashHighlightIndex, input: input, inputHint: inputHint, slashMenu: slashMenu, onApprovalDecision: (decision) => approvals.decide(decision), onModelSelect: (name) => {
|
|
1172
|
+
setPicker(null);
|
|
1173
|
+
void handleModelCommand(name);
|
|
1174
|
+
}, onYoloSelect: (value) => {
|
|
1175
|
+
setPicker(null);
|
|
1176
|
+
void handleYoloCommand(value);
|
|
1177
|
+
}, onModeSelect: (value) => {
|
|
1178
|
+
setPicker(null);
|
|
1179
|
+
void handleModeCommand(value);
|
|
1180
|
+
}, onInputChange: setInput, onSubmit: onSubmit })] })] }));
|
|
1077
1181
|
}
|
|
1078
1182
|
//# sourceMappingURL=app.js.map
|