osai-agent 4.0.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.
Files changed (86) hide show
  1. package/LICENSE +7 -0
  2. package/package.json +72 -0
  3. package/src/agent/context.js +141 -0
  4. package/src/agent/loop/context-summary.js +196 -0
  5. package/src/agent/loop/directory-utils.js +102 -0
  6. package/src/agent/loop/local.js +196 -0
  7. package/src/agent/loop/loop-detection.js +288 -0
  8. package/src/agent/loop/stream-parser.js +515 -0
  9. package/src/agent/loop/tool-executor.js +470 -0
  10. package/src/agent/loop/verification.js +263 -0
  11. package/src/agent/loop/websocket.js +80 -0
  12. package/src/agent/prompt.js +259 -0
  13. package/src/agent/react-loop.js +697 -0
  14. package/src/agent/subagent.js +263 -0
  15. package/src/commands/config.js +53 -0
  16. package/src/commands/connect.js +190 -0
  17. package/src/commands/devices.js +121 -0
  18. package/src/commands/login.js +77 -0
  19. package/src/commands/logout.js +31 -0
  20. package/src/commands/mcp.js +258 -0
  21. package/src/commands/provider.js +633 -0
  22. package/src/commands/register.js +74 -0
  23. package/src/commands/run.js +150 -0
  24. package/src/commands/search.js +64 -0
  25. package/src/commands/session.js +57 -0
  26. package/src/commands/skills.js +54 -0
  27. package/src/commands/stop-subagent.js +58 -0
  28. package/src/index.js +208 -0
  29. package/src/llm/direct.js +317 -0
  30. package/src/memory/store.js +215 -0
  31. package/src/mock-readline.js +27 -0
  32. package/src/parser/dependencies.js +71 -0
  33. package/src/parser/markdown.js +505 -0
  34. package/src/parser/stream.js +96 -0
  35. package/src/prompts/modes/CODING.js +160 -0
  36. package/src/prompts/modes/GENERAL.js +105 -0
  37. package/src/prompts/modes/NETWORK.js +69 -0
  38. package/src/prompts/modes/SSH.js +53 -0
  39. package/src/prompts/systemPrompt.js +85 -0
  40. package/src/safety/check.js +210 -0
  41. package/src/services/crypto.js +78 -0
  42. package/src/services/executor.js +68 -0
  43. package/src/services/history.js +58 -0
  44. package/src/services/server-url.js +11 -0
  45. package/src/services/session.js +194 -0
  46. package/src/services/ssh.js +176 -0
  47. package/src/services/websocket.js +112 -0
  48. package/src/skills/loader.js +231 -0
  49. package/src/tools/browser.js +434 -0
  50. package/src/tools/local.js +1254 -0
  51. package/src/tools/mcp-client.js +209 -0
  52. package/src/tools/registry.js +132 -0
  53. package/src/tools/search-providers.js +237 -0
  54. package/src/tools/ssh.js +74 -0
  55. package/src/ui/App.js +2031 -0
  56. package/src/ui/animation.js +47 -0
  57. package/src/ui/components/AskUserDialog.js +33 -0
  58. package/src/ui/components/ConfirmationDialog.js +45 -0
  59. package/src/ui/components/DiffView.js +201 -0
  60. package/src/ui/components/Header.js +157 -0
  61. package/src/ui/components/HistoryPicker.js +130 -0
  62. package/src/ui/components/InputShell.js +22 -0
  63. package/src/ui/components/MessageHistory.js +1200 -0
  64. package/src/ui/components/ModalPanel.js +40 -0
  65. package/src/ui/components/ModePicker.js +161 -0
  66. package/src/ui/components/PlanDialog.js +48 -0
  67. package/src/ui/components/ProviderMenu.js +1095 -0
  68. package/src/ui/components/SavePicker.js +106 -0
  69. package/src/ui/components/SelectMenu.js +194 -0
  70. package/src/ui/components/SlashMenu.js +168 -0
  71. package/src/ui/components/SubagentPanel.js +138 -0
  72. package/src/ui/components/TextInputSafe.js +117 -0
  73. package/src/ui/components/TodoPanel.js +54 -0
  74. package/src/ui/components/ToolExecution.js +261 -0
  75. package/src/ui/components/TranscriptViewport.js +99 -0
  76. package/src/ui/diff.js +249 -0
  77. package/src/ui/h.js +7 -0
  78. package/src/ui/mouse-scroll.js +63 -0
  79. package/src/ui/slash-picker.js +58 -0
  80. package/src/ui/terminal.js +41 -0
  81. package/src/ui/theme.js +5 -0
  82. package/src/ui/welcome.js +12 -0
  83. package/src/utils/constants.js +231 -0
  84. package/src/utils/helpers.js +154 -0
  85. package/src/utils/logger.js +81 -0
  86. package/src/utils/sound.js +33 -0
@@ -0,0 +1,47 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const ENABLE_UI_ANIMATIONS = process.env.OSAI_UI_ANIMATIONS !== '0';
4
+
5
+ const defaultInterval = process.platform !== 'win32' && process.stdout.isTTY ? '800' : '400';
6
+ const requestedInterval = Number.parseInt(process.env.OSAI_UI_ANIMATION_INTERVAL_MS || defaultInterval, 10);
7
+ const TICK_INTERVAL_MS = Number.isFinite(requestedInterval)
8
+ ? Math.max(300, requestedInterval)
9
+ : 400;
10
+ // Astuce : augmentez OSAI_UI_ANIMATION_INTERVAL_MS (ex: 2000) pour ralentir les spinners si le terminal tremble
11
+ // Ou désactivez toutes les animations : OSAI_UI_ANIMATIONS=0
12
+
13
+ let frame = 0;
14
+ let timer = null;
15
+ const subscribers = new Set();
16
+
17
+ function startTicker() {
18
+ if (timer || !ENABLE_UI_ANIMATIONS) return;
19
+ timer = setInterval(() => {
20
+ frame += 1;
21
+ for (const notify of subscribers) notify(frame);
22
+ }, TICK_INTERVAL_MS);
23
+ timer.unref?.();
24
+ }
25
+
26
+ function stopTicker() {
27
+ if (timer && subscribers.size === 0) {
28
+ clearInterval(timer);
29
+ timer = null;
30
+ }
31
+ }
32
+
33
+ export function useAnimationFrame(enabled = true) {
34
+ const [currentFrame, setCurrentFrame] = useState(frame);
35
+
36
+ useEffect(() => {
37
+ if (!enabled || !ENABLE_UI_ANIMATIONS) return;
38
+ subscribers.add(setCurrentFrame);
39
+ startTicker();
40
+ return () => {
41
+ subscribers.delete(setCurrentFrame);
42
+ stopTicker();
43
+ };
44
+ }, [enabled]);
45
+
46
+ return ENABLE_UI_ANIMATIONS ? currentFrame : 0;
47
+ }
@@ -0,0 +1,33 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from './TextInputSafe.js';
4
+ import { h } from '../h.js';
5
+ import { InputShell } from './InputShell.js';
6
+
7
+ export function AskUserDialog({ question, options, onSubmit }) {
8
+ const [value, setValue] = useState('');
9
+ const Input = TextInput.default || TextInput;
10
+
11
+ return h(Box, { flexDirection: 'column', paddingY: 1, borderStyle: 'round', borderColor: '#7aa2f7' },
12
+ h(Text, { color: '#7aa2f7', bold: true }, 'Question from agent'),
13
+ h(Box, { marginTop: 1 },
14
+ h(Text, { color: '#c0caf5' }, question)
15
+ ),
16
+ options && options.length > 0 ? h(Box, { flexDirection: 'column', marginTop: 1 },
17
+ ...options.map((opt, i) =>
18
+ h(Text, { key: i, color: '#9ece6a' }, ` ${i + 1}. ${opt}`)
19
+ )
20
+ ) : null,
21
+ h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
22
+ h(Text, { color: '#7aa2f7' }, '> '),
23
+ h(Input, {
24
+ value,
25
+ onChange: setValue,
26
+ onSubmit: () => {
27
+ onSubmit(value);
28
+ setValue('');
29
+ }
30
+ })
31
+ )
32
+ );
33
+ }
@@ -0,0 +1,45 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { InputShell } from './InputShell.js';
5
+ import { isOnlySgrMouseInput } from '../mouse-scroll.js';
6
+
7
+ export function ConfirmationDialog({ promptText, details, onConfirm }) {
8
+ const [value, setValue] = useState('');
9
+
10
+ useInput((input, key) => {
11
+ if (key.return) {
12
+ onConfirm(value);
13
+ setValue('');
14
+ return;
15
+ }
16
+ if (key.backspace || key.delete) {
17
+ setValue(v => v.slice(0, -1));
18
+ return;
19
+ }
20
+ if (!isOnlySgrMouseInput(input)) {
21
+ setValue(v => v + input);
22
+ }
23
+ });
24
+
25
+ const isDangerous = details?.isDangerous || details?.tier === 'DANGEROUS';
26
+ const tool = details?.tool || '';
27
+ const identifier = details?.identifier || '';
28
+ const description = details?.description || '';
29
+
30
+ const borderColor = isDangerous ? 'red' : 'yellow';
31
+ const headerColor = isDangerous ? '#f7768e' : '#e0af68';
32
+ const toolColor = isDangerous ? '#f7768e' : '#7dcfff';
33
+
34
+ return h(Box, { flexDirection: 'column', paddingY: 1, borderStyle: 'round', borderColor },
35
+ details ? h(Box, { flexDirection: 'column', marginBottom: 1 },
36
+ h(Text, { color: headerColor, bold: true }, isDangerous ? ' DANGEROUS OPERATION ' : ' Confirmation required '),
37
+ h(Text, { color: toolColor }, `[${tool}] ${identifier}`),
38
+ h(Text, { color: '#9aa5ce' }, description)
39
+ ) : h(Text, { color: 'white', bold: true }, promptText),
40
+ h(InputShell, { flexDirection: 'row', marginTop: 1, paddingX: 1 },
41
+ h(Text, { color: isDangerous ? '#f7768e' : '#e0af68' }, isDangerous ? 'Type "confirm" to proceed: ' : 'Run this? (y/N): '),
42
+ h(Text, { color: '#c0caf5' }, value || ' ')
43
+ )
44
+ );
45
+ }
@@ -0,0 +1,201 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { buildDiff, collapseContext, langFromPath } from '../diff.js';
5
+ import { highlightCode } from '../../parser/markdown.js';
6
+
7
+ const C = {
8
+ removedBg: '#2d1010',
9
+ removedFg: '#f7768e',
10
+ addedBg: '#1a2f1a',
11
+ addedFg: '#9ece6a',
12
+ ctxFg: '#565f89',
13
+ lineNumFg: '#414868',
14
+ border: '#2a2e3f',
15
+ headerBg: '#1a1b26',
16
+ headerFg: '#7aa2f7',
17
+ dim: '#414868',
18
+ addedLabel: '#9ece6a',
19
+ removedLabel: '#f7768e',
20
+ };
21
+
22
+ function wrapPlain(line, width) {
23
+ const safeWidth = Math.max(1, width | 0);
24
+ const src = String(line ?? '');
25
+ if (!src.length) return [''];
26
+ const out = [];
27
+ for (let i = 0; i < src.length; i += safeWidth) out.push(src.slice(i, i + safeWidth));
28
+ return out;
29
+ }
30
+
31
+ function lineNumPart(oldN, newN, lnW) {
32
+ const o = oldN ? String(oldN).padStart(lnW) : ' '.repeat(lnW);
33
+ const n = newN ? String(newN).padStart(lnW) : ' '.repeat(lnW);
34
+ return h(Text, { color: C.lineNumFg }, `${o}:${n}`);
35
+ }
36
+
37
+ function DiffLine({ hunk, lnW, innerW, lang }) {
38
+ const oldNum = hunk.oldN || null;
39
+ const newNum = hunk.newN || null;
40
+ const chunks = wrapPlain(hunk.line, innerW);
41
+
42
+ if (hunk.type === 'ctx') {
43
+ return h(Box, { flexDirection: 'column' },
44
+ ...chunks.map((chunk, i) =>
45
+ h(Box, { key: `ctx_${i}`, paddingLeft: 1 },
46
+ lineNumPart(i === 0 ? oldNum : null, i === 0 ? newNum : null, lnW),
47
+ h(Text, { color: C.ctxFg }, ' ' + chunk)
48
+ )
49
+ )
50
+ );
51
+ }
52
+
53
+ if (hunk.type === 'del') {
54
+ return h(Box, { flexDirection: 'column' },
55
+ ...chunks.map((chunk, i) => {
56
+ const highlighted = highlightCode(chunk, lang);
57
+ return h(Box, { key: `del_${i}`, paddingLeft: 1 },
58
+ lineNumPart(i === 0 ? oldNum : null, i === 0 ? newNum : null, lnW),
59
+ h(Box, { backgroundColor: C.removedBg },
60
+ h(Text, { color: C.removedFg }, i === 0 ? '-' : ' '),
61
+ h(Text, { ansi: true }, highlighted)
62
+ )
63
+ );
64
+ })
65
+ );
66
+ }
67
+
68
+ if (hunk.type === 'add') {
69
+ return h(Box, { flexDirection: 'column' },
70
+ ...chunks.map((chunk, i) => {
71
+ const highlighted = highlightCode(chunk, lang);
72
+ return h(Box, { key: `add_${i}`, paddingLeft: 1 },
73
+ lineNumPart(i === 0 ? oldNum : null, i === 0 ? newNum : null, lnW),
74
+ h(Box, { backgroundColor: C.addedBg },
75
+ h(Text, { color: C.addedFg }, i === 0 ? '+' : ' '),
76
+ h(Text, { ansi: true }, highlighted)
77
+ )
78
+ );
79
+ })
80
+ );
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ function getInnerWidth(lnW) {
87
+ const cols = process.stdout?.columns || 120;
88
+ return Math.max(24, cols - (lnW * 2 + 12));
89
+ }
90
+
91
+ function SkipLine({ count }) {
92
+ const msg = ` ··· ${count} unchanged line${count > 1 ? 's' : ''}`;
93
+ return h(Box, { paddingLeft: 2 },
94
+ h(Text, { color: C.dim }, msg)
95
+ );
96
+ }
97
+
98
+ export function EditFileDiff({ filePath, find, replace }) {
99
+ if (!find && !replace) return null;
100
+ const hunks = buildDiff(find || '', replace || '');
101
+ const collapsed = collapseContext(hunks, 3);
102
+ const allLines = [...hunks.filter(h => h.oldN || h.newN)];
103
+ const maxN = Math.max(...allLines.map(h => Math.max(h.oldN || 0, h.newN || 0)), 1);
104
+ const lnW = String(maxN).length;
105
+ const innerW = getInnerWidth(lnW);
106
+ const lang = langFromPath(filePath);
107
+ const dels = hunks.filter(h => h.type === 'del').length;
108
+ const adds = hunks.filter(h => h.type === 'add').length;
109
+
110
+ return h(Box, { flexDirection: 'column', marginY: 1 },
111
+ h(Box, { paddingLeft: 2, paddingY: 0 },
112
+ h(Text, { color: C.headerFg, bold: true }, ' ✎ '),
113
+ h(Text, { color: C.headerFg }, filePath || '')
114
+ ),
115
+ h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
116
+ ...collapsed.map((hunk, i) => {
117
+ if (hunk.type === 'skip') return h(SkipLine, { key: i, count: hunk.count });
118
+ return h(DiffLine, { key: i, hunk, lnW, innerW, lang });
119
+ }),
120
+ h(Box, { marginTop: 1 },
121
+ adds > 0 ? h(Text, { color: C.addedLabel }, `+${adds} `) : null,
122
+ dels > 0 ? h(Text, { color: C.removedLabel }, `-${dels}`) : null
123
+ )
124
+ )
125
+ );
126
+ }
127
+
128
+ export function NewFileDiff({ filePath, content }) {
129
+ if (!content) return null;
130
+ const lines = (content || '').split('\n');
131
+ const lnW = String(lines.length).length;
132
+ const innerW = 60;
133
+ const lang = langFromPath(filePath);
134
+ const shown = Math.min(lines.length, 50);
135
+
136
+ return h(Box, { flexDirection: 'column', marginY: 1 },
137
+ h(Box, { paddingLeft: 2, paddingY: 0 },
138
+ h(Text, { color: C.addedLabel, bold: true }, ' + '),
139
+ h(Text, { color: C.addedLabel }, `${filePath} (new file, ${lines.length} lines)`)
140
+ ),
141
+ h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
142
+ ...Array.from({ length: shown }, (_, i) => {
143
+ const raw = lines[i].length > innerW ? lines[i].slice(0, innerW - 1) + '…' : lines[i];
144
+ return h(Box, { key: i, paddingLeft: 1 },
145
+ h(Text, { color: C.lineNumFg }, `${String(i + 1).padStart(lnW)} `),
146
+ h(Box, { backgroundColor: C.addedBg },
147
+ h(Text, { color: C.addedFg }, '+ '),
148
+ h(Text, { ansi: true }, highlightCode(raw, lang))
149
+ )
150
+ );
151
+ }),
152
+ lines.length > 50 ? h(Box, { paddingLeft: 2 },
153
+ h(Text, { color: C.dim }, ` ··· ${lines.length - 50} more lines`)
154
+ ) : null
155
+ )
156
+ );
157
+ }
158
+
159
+ export function AppendFileDiff({ filePath, content }) {
160
+ if (!content) return null;
161
+ const lines = (content || '').split('\n');
162
+ const lnW = String(lines.length).length;
163
+ const innerW = 60;
164
+ const lang = langFromPath(filePath);
165
+ const skip = lines.length > 30;
166
+ const shown = skip ? lines.slice(0, 15) : lines;
167
+
168
+ return h(Box, { flexDirection: 'column', marginY: 1 },
169
+ h(Box, { paddingLeft: 2, paddingY: 0 },
170
+ h(Text, { color: C.addedLabel, bold: true }, ' + '),
171
+ h(Text, { color: C.addedLabel }, `${filePath} (appending ${lines.length} line${lines.length > 1 ? 's' : ''})`)
172
+ ),
173
+ h(Box, { flexDirection: 'column', paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
174
+ ...shown.map((line, i) => {
175
+ const raw = line.length > innerW ? line.slice(0, innerW - 1) + '…' : line;
176
+ return h(Box, { key: i, paddingLeft: 1 },
177
+ h(Text, { color: C.lineNumFg }, `${String(i + 1).padStart(lnW)} `),
178
+ h(Box, { backgroundColor: C.addedBg },
179
+ h(Text, { color: C.addedFg }, '+ '),
180
+ h(Text, { ansi: true }, highlightCode(raw, lang))
181
+ )
182
+ );
183
+ }),
184
+ skip ? h(Box, { paddingLeft: 2 },
185
+ h(Text, { color: C.dim }, ` ··· ${lines.length - 15} more lines`)
186
+ ) : null
187
+ )
188
+ );
189
+ }
190
+
191
+ export function DeleteFileDiff({ filePath }) {
192
+ return h(Box, { flexDirection: 'column', marginY: 1 },
193
+ h(Box, { paddingLeft: 2, paddingY: 0 },
194
+ h(Text, { color: C.removedLabel, bold: true }, ' ✗ '),
195
+ h(Text, { color: C.removedLabel }, `${filePath} (deleted)`)
196
+ ),
197
+ h(Box, { paddingLeft: 2, borderStyle: 'round', borderColor: C.border },
198
+ h(Text, { color: C.removedLabel }, ' File deleted')
199
+ )
200
+ );
201
+ }
@@ -0,0 +1,157 @@
1
+ import { Box, Text } from 'ink';
2
+ import { h } from '../h.js';
3
+ import { ENABLE_UI_ANIMATIONS, useAnimationFrame } from '../animation.js';
4
+
5
+ const chunkLength = (chunks) => chunks.reduce((sum, chunk) => sum + String(chunk.text || '').length, 0);
6
+
7
+ const makeItem = (chunks) => ({ chunks, width: chunkLength(chunks) });
8
+
9
+ function truncateChunks(chunks, maxWidth) {
10
+ if (chunkLength(chunks) <= maxWidth) return chunks;
11
+ if (maxWidth <= 1) return [{ text: '…', color: chunks[0]?.color || 'white' }];
12
+
13
+ const out = [];
14
+ let remaining = maxWidth;
15
+ for (const chunk of chunks) {
16
+ if (remaining <= 0) break;
17
+ const text = String(chunk.text || '');
18
+ if (text.length <= remaining) {
19
+ out.push(chunk);
20
+ remaining -= text.length;
21
+ continue;
22
+ }
23
+ out.push({ ...chunk, text: text.slice(0, Math.max(0, remaining - 1)) + '…' });
24
+ remaining = 0;
25
+ }
26
+ return out;
27
+ }
28
+
29
+ function wrapItems(items, width) {
30
+ const safeWidth = Math.max(16, Number(width) || 80);
31
+ const lines = [];
32
+ let current = [];
33
+ let currentWidth = 0;
34
+
35
+ for (const item of items) {
36
+ const safeItem = item.width > safeWidth
37
+ ? makeItem(truncateChunks(item.chunks, safeWidth))
38
+ : item;
39
+
40
+ if (current.length > 0 && currentWidth + safeItem.width > safeWidth) {
41
+ lines.push(current);
42
+ current = [];
43
+ currentWidth = 0;
44
+ }
45
+ current.push(safeItem);
46
+ currentWidth += safeItem.width;
47
+ }
48
+
49
+ if (current.length > 0) lines.push(current);
50
+ return lines;
51
+ }
52
+
53
+ export function getHeaderRows(columns = 80) {
54
+ const contentWidth = Math.max(16, (columns || 80) - 2);
55
+ const estimatedWidth = 104;
56
+ return Math.ceil(estimatedWidth / contentWidth) + 2;
57
+ }
58
+
59
+ export function Header({ mode, device, isConnected, isLocal, provider, executionMode, tokenCount, subagentActive, columns }) {
60
+ const frame = useAnimationFrame(!!subagentActive);
61
+ const providerType = provider?.type || 'osai';
62
+ const providerModel = provider?.model || null;
63
+ const isDefault = providerType === 'osai' || !providerType;
64
+ const subagentDim = ENABLE_UI_ANIMATIONS && subagentActive ? frame % 2 === 1 : false;
65
+ const contentWidth = Math.max(16, (columns || 80) - 2);
66
+
67
+ const tokenLabel = tokenCount != null
68
+ ? tokenCount >= 1000
69
+ ? `~${(tokenCount / 1000).toFixed(1)}k tokens`
70
+ : `~${tokenCount} tokens`
71
+ : null;
72
+
73
+ const statusChunks = isLocal
74
+ ? [{ text: '\u25CF Local', color: '#73daca', bold: true }]
75
+ : [{ text: isConnected ? '\u25CF Connected' : '\u25CB Disconnected', color: isConnected ? '#9ece6a' : '#f7768e', bold: true }];
76
+
77
+ const items = [
78
+ makeItem([
79
+ { text: '\u2590', color: '#7aa2f7' },
80
+ { text: '\u25B3', color: '#4a9eff', bold: true },
81
+ { text: '\u258C ', color: '#7aa2f7' },
82
+ { text: 'OS AI AGENT ', color: '#c0caf5', bold: true },
83
+ ]),
84
+ makeItem([
85
+ { text: '\u2502 ', color: '#3b4261' },
86
+ { text: 'Provider: ', color: '#565f89' },
87
+ {
88
+ text: isDefault ? (isLocal ? 'not configured ' : 'osai/auto ') : `${providerType}${providerModel ? '/' + providerModel : ''} `,
89
+ color: isDefault ? '#565f89' : '#9ece6a',
90
+ bold: !isDefault,
91
+ },
92
+ ]),
93
+ makeItem([
94
+ { text: '\u2502 ', color: '#3b4261' },
95
+ { text: 'Mode: ', color: '#565f89' },
96
+ { text: `${mode}/${executionMode || 'EXEC'} `, color: '#c0caf5' },
97
+ ]),
98
+ makeItem([
99
+ { text: '\u2502 ', color: '#3b4261' },
100
+ { text: 'Device: ', color: '#565f89' },
101
+ { text: `${device || 'local'} `, color: '#c0caf5' },
102
+ ]),
103
+ makeItem([
104
+ { text: '\u2502 ', color: '#3b4261' },
105
+ ...statusChunks,
106
+ ]),
107
+ ];
108
+
109
+ if (subagentActive) {
110
+ items.push(makeItem([
111
+ { text: ' \u2502 ', color: '#3b4261' },
112
+ { text: '\u25CF Subagent', color: subagentDim ? '#1a5fad' : '#4a9eff', bold: !subagentDim },
113
+ ]));
114
+ }
115
+
116
+ if (tokenLabel) {
117
+ items.push(makeItem([
118
+ { text: ' \u2502 ', color: '#3b4261' },
119
+ { text: tokenLabel, color: '#e0af68' },
120
+ ]));
121
+ }
122
+
123
+ const lines = wrapItems(items, contentWidth);
124
+ const renderLine = (line, lineIndex) => {
125
+ const indentWidth = lineIndex > 0 ? 2 : 0;
126
+ const lineWidth = chunkLength(line.flatMap((item) => item.chunks)) + indentWidth;
127
+ const padding = ' '.repeat(Math.max(0, contentWidth - lineWidth));
128
+
129
+ return h(Box, { key: `header_line_${lineIndex}`, flexDirection: 'row', width: '100%' },
130
+ h(Text, { color: 'white' }, '│'),
131
+ lineIndex > 0 ? h(Text, { color: '#3b4261' }, ' ') : null,
132
+ line.map((item, itemIndex) =>
133
+ h(Text, { key: `header_item_${lineIndex}_${itemIndex}` },
134
+ item.chunks.map((chunk, chunkIndex) =>
135
+ h(Text, {
136
+ key: `header_chunk_${lineIndex}_${itemIndex}_${chunkIndex}`,
137
+ color: chunk.color,
138
+ bold: chunk.bold,
139
+ }, chunk.text)
140
+ )
141
+ )
142
+ ),
143
+ h(Text, {}, padding),
144
+ h(Text, { color: 'white' }, '│')
145
+ );
146
+ };
147
+
148
+ return h(Box, {
149
+ flexShrink: 0,
150
+ width: '100%',
151
+ flexDirection: 'column',
152
+ },
153
+ h(Text, { color: 'white' }, `┌${'─'.repeat(contentWidth)}┐`),
154
+ lines.map(renderLine),
155
+ h(Text, { color: 'white' }, `└${'─'.repeat(contentWidth)}┘`)
156
+ );
157
+ }
@@ -0,0 +1,130 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, useInput, useWindowSize } from 'ink';
3
+ import { h } from '../h.js';
4
+ import { InputShell } from './InputShell.js';
5
+ import { isOnlySgrMouseInput } from '../mouse-scroll.js';
6
+
7
+ export function HistoryPicker({ sessions, visible, onSelect, onCancel }) {
8
+ const [cursor, setCursor] = useState(0);
9
+ const [query, setQuery] = useState('');
10
+
11
+ const maxDisplay = 15;
12
+
13
+ const filtered = useMemo(() => {
14
+ if (!query) return sessions.slice(0, maxDisplay);
15
+ const q = query.toLowerCase();
16
+ return sessions
17
+ .filter(s =>
18
+ (s.title || '').toLowerCase().includes(q) ||
19
+ (s.mode || '').toLowerCase().includes(q) ||
20
+ (s.storage || '').toLowerCase().includes(q)
21
+ )
22
+ .slice(0, maxDisplay);
23
+ }, [sessions, query]);
24
+
25
+ useEffect(() => {
26
+ if (cursor >= filtered.length) setCursor(Math.max(0, filtered.length - 1));
27
+ }, [filtered.length]);
28
+
29
+ useInput((input, key) => {
30
+ if (!visible) return;
31
+
32
+ if (key.escape) {
33
+ setQuery('');
34
+ setCursor(0);
35
+ onCancel();
36
+ return;
37
+ }
38
+
39
+ if (key.upArrow) {
40
+ setCursor(c => (c > 0 ? c - 1 : filtered.length - 1));
41
+ return;
42
+ }
43
+
44
+ if (key.downArrow) {
45
+ setCursor(c => (c < filtered.length - 1 ? c + 1 : 0));
46
+ return;
47
+ }
48
+
49
+ if (key.return) {
50
+ if (filtered.length > 0) {
51
+ onSelect(filtered[cursor]);
52
+ }
53
+ setQuery('');
54
+ setCursor(0);
55
+ return;
56
+ }
57
+
58
+ if (key.backspace || key.delete) {
59
+ setQuery(q => q.slice(0, -1));
60
+ setCursor(0);
61
+ return;
62
+ }
63
+
64
+ if (isOnlySgrMouseInput(input)) return;
65
+ if (input && !key.ctrl && !key.meta) {
66
+ setQuery(q => q + input);
67
+ setCursor(0);
68
+ }
69
+ });
70
+
71
+ if (!visible) return null;
72
+
73
+ const { rows } = useWindowSize();
74
+ const maxVis = Math.min(filtered.length, Math.max(5, (rows || 24) - 10));
75
+ let startIdx = 0;
76
+ if (filtered.length > maxVis) {
77
+ const half = Math.floor(maxVis / 2);
78
+ startIdx = Math.max(0, Math.min(cursor - half, filtered.length - maxVis));
79
+ }
80
+ const visibleItems = filtered.slice(startIdx, startIdx + maxVis);
81
+
82
+ const separator = '─'.repeat(52);
83
+
84
+ return h(
85
+ Box,
86
+ { flexDirection: 'column', borderStyle: 'round', borderColor: '#2a2e3f', paddingX: 1, paddingY: 0, marginY: 1 },
87
+
88
+ h(Box, { flexDirection: 'row', alignItems: 'center' },
89
+ h(Text, { color: '#7aa2f7', bold: true }, ' Resume a saved session'),
90
+ h(Text, { color: '#565f89', dimColor: true }, ` ${filtered.length} sessions`)
91
+ ),
92
+ h(Text, { color: '#3b3f52' }, separator),
93
+
94
+ h(InputShell, { flexDirection: 'row', paddingY: 0, marginY: 0 },
95
+ h(Text, { color: '#e0af68' }, ' Search: '),
96
+ h(Text, { color: '#c0caf5' }, query || ' '),
97
+ h(Text, { color: '#565f89' }, query ? '' : ' type to filter sessions')
98
+ ),
99
+ h(Text, { color: '#3b3f52' }, separator),
100
+
101
+ ...visibleItems.map((s, i) => {
102
+ const realIdx = startIdx + i;
103
+ const isHL = realIdx === cursor;
104
+ const prefix = isHL ? h(Text, { color: '#9ece6a' }, ' ▸ ') : h(Text, { color: '#3b3f52' }, ' ');
105
+ const storageIcon = s.storage === 'cloud' ? '☁ ' : ' ';
106
+
107
+ return h(Box, { key: realIdx },
108
+ prefix,
109
+ h(Text, isHL ? { color: '#ffffff', bold: true, backgroundColor: '#2a3a5c' } : { color: '#9aa5ce' },
110
+ ` ${storageIcon}[${(s.storage || 'local').toUpperCase()}] ${s.title || 'Untitled'} - ${s.mode} - ${s.messageCount} msgs - ${new Date(s.savedAt).toLocaleDateString()}`
111
+ )
112
+ );
113
+ }),
114
+
115
+ filtered.length > maxVis
116
+ ? h(Text, { color: '#565f89', dimColor: true }, ` Showing ${startIdx + 1}-${startIdx + visibleItems.length} of ${filtered.length}`)
117
+ : null,
118
+
119
+ h(Text, { color: '#3b3f52' }, separator),
120
+
121
+ h(
122
+ Box,
123
+ { flexDirection: 'row' },
124
+ h(Text, { color: '#9ece6a' }, ' ↑↓'), h(Text, { color: '#565f89' }, ' Navigate'),
125
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Enter'), h(Text, { color: '#565f89' }, ' Resume'),
126
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Esc'), h(Text, { color: '#565f89' }, ' Cancel'),
127
+ h(Text, { color: '#3b3f52' }, ' '), h(Text, { color: '#9ece6a' }, ' Type'), h(Text, { color: '#565f89' }, ' Search')
128
+ )
129
+ );
130
+ }
@@ -0,0 +1,22 @@
1
+ import { Box } from 'ink';
2
+ import { h } from '../h.js';
3
+ import { INPUT_BACKGROUND } from '../theme.js';
4
+
5
+ export function InputShell({
6
+ children,
7
+ paddingX = 1,
8
+ paddingY = 0,
9
+ width = '100%',
10
+ marginY = 0,
11
+ flexDirection = 'column',
12
+ backgroundColor = INPUT_BACKGROUND,
13
+ }) {
14
+ return h(Box, {
15
+ backgroundColor,
16
+ paddingX,
17
+ paddingY,
18
+ width,
19
+ marginY,
20
+ flexDirection,
21
+ }, children);
22
+ }