@denizokcu/haze 0.0.2 → 0.0.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.
- package/CHANGELOG.md +10 -0
- package/README.md +87 -33
- package/dist/cli/commands/chat.d.ts +3 -1
- package/dist/cli/commands/chat.js +442 -52
- package/dist/cli/commands/commands.d.ts +5 -0
- package/dist/cli/commands/commands.js +114 -29
- package/dist/cli/commands/formatters.js +5 -2
- package/dist/cli/commands/streaming.d.ts +5 -1
- package/dist/cli/commands/streaming.js +193 -86
- package/dist/cli/index.js +5 -2
- package/dist/config/inputHistory.js +8 -0
- package/dist/config/providers.d.ts +26 -0
- package/dist/config/providers.js +88 -0
- package/dist/config/settings.d.ts +9 -2
- package/dist/core/agent/compaction.d.ts +13 -0
- package/dist/core/agent/compaction.js +34 -0
- package/dist/core/agent/errors.d.ts +3 -0
- package/dist/core/agent/errors.js +13 -0
- package/dist/core/agent/events.d.ts +58 -0
- package/dist/core/agent/events.js +3 -0
- package/dist/core/goal/completionPolicy.d.ts +27 -0
- package/dist/core/goal/completionPolicy.js +67 -0
- package/dist/core/goal/requestClassifier.d.ts +6 -0
- package/dist/core/goal/requestClassifier.js +31 -0
- package/dist/core/goal/sessionGoal.d.ts +30 -0
- package/dist/core/goal/sessionGoal.js +88 -0
- package/dist/core/session/sessionStore.d.ts +37 -0
- package/dist/core/session/sessionStore.js +59 -0
- package/dist/llm/client.d.ts +1 -1
- package/dist/llm/client.js +6 -6
- package/dist/llm/hazeTools.d.ts +38 -0
- package/dist/llm/hazeTools.js +196 -92
- package/dist/llm/initPrompt.js +6 -4
- package/dist/llm/systemPrompt.js +3 -3
- package/dist/skills/builder/SkillBuilder.d.ts +6 -0
- package/dist/skills/builder/SkillBuilder.js +146 -24
- package/dist/ui/components/ErrorView.d.ts +2 -1
- package/dist/ui/components/Header.d.ts +2 -1
- package/dist/ui/components/Header.js +1 -11
- package/dist/ui/components/MarkdownText.d.ts +2 -1
- package/dist/ui/components/TextInput.d.ts +7 -3
- package/dist/ui/components/TextInput.js +112 -27
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.js +2 -1
- package/package.json +8 -8
|
@@ -2,11 +2,12 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
|
-
import { Box, render, Text, useApp, useStdout } from 'ink';
|
|
5
|
+
import { Box, render, Static, Text, useApp, useStdout } from 'ink';
|
|
6
6
|
import Spinner from 'ink-spinner';
|
|
7
7
|
import { readContextFiles } from '../../config/contextFiles.js';
|
|
8
8
|
import { addInputHistoryItem, readInputHistory } from '../../config/inputHistory.js';
|
|
9
9
|
import { readSettings, updateSettings } from '../../config/settings.js';
|
|
10
|
+
import { activeModel, configuredProviders, DEFAULT_PROVIDER_NAME, findProvider, modelSelector, providerHasKey, resolveModelSelector, upsertProvider } from '../../config/providers.js';
|
|
10
11
|
import { Header } from '../../ui/components/Header.js';
|
|
11
12
|
import { TextInput } from '../../ui/components/TextInput.js';
|
|
12
13
|
import { MarkdownText } from '../../ui/components/MarkdownText.js';
|
|
@@ -14,6 +15,9 @@ import { theme } from '../../ui/theme.js';
|
|
|
14
15
|
import { handleSlashCommand } from './commands.js';
|
|
15
16
|
import { runAgentTurn } from './streaming.js';
|
|
16
17
|
import { loadSkillRegistry } from '../../skills/SkillRegistry.js';
|
|
18
|
+
import { appendSessionEntry, createSession, formatSession, latestSession, restoreConversation } from '../../core/session/sessionStore.js';
|
|
19
|
+
import { compactModelMessages, modelMessageText } from '../../core/agent/compaction.js';
|
|
20
|
+
import { createSessionGoal, formatGoalStatus } from '../../core/goal/sessionGoal.js';
|
|
17
21
|
const execFile = promisify(execFileCallback);
|
|
18
22
|
async function currentBranchName() {
|
|
19
23
|
try {
|
|
@@ -45,6 +49,19 @@ function formatTokenCount(tokens) {
|
|
|
45
49
|
return `${(tokens / 1_000).toFixed(tokens >= 10_000 ? 0 : 1).replace(/\.0$/, '')}k`;
|
|
46
50
|
return String(tokens);
|
|
47
51
|
}
|
|
52
|
+
function truncateWithEllipsis(text, maxLength) {
|
|
53
|
+
if (text.length <= maxLength)
|
|
54
|
+
return text;
|
|
55
|
+
return `${text.slice(0, maxLength).trimEnd()}…`;
|
|
56
|
+
}
|
|
57
|
+
function displayMessagesFromConversation(conversation) {
|
|
58
|
+
return conversation.flatMap(message => {
|
|
59
|
+
if (message.role !== 'user' && message.role !== 'assistant')
|
|
60
|
+
return [];
|
|
61
|
+
const text = modelMessageText(message).trim();
|
|
62
|
+
return text ? [{ role: message.role, text }] : [];
|
|
63
|
+
});
|
|
64
|
+
}
|
|
48
65
|
function estimateConversationTokens(messages) {
|
|
49
66
|
const inputText = messages
|
|
50
67
|
.filter(message => message.role === 'user' || message.role === 'tool')
|
|
@@ -59,6 +76,14 @@ function estimateConversationTokens(messages) {
|
|
|
59
76
|
output: estimateTokens(outputText),
|
|
60
77
|
};
|
|
61
78
|
}
|
|
79
|
+
function fullWidthLines(text, width, leftPadding = 0) {
|
|
80
|
+
const safeWidth = Math.max(1, width);
|
|
81
|
+
const prefix = ' '.repeat(leftPadding);
|
|
82
|
+
return text.replace(/\r\n|\r/g, '\n').split('\n').map(line => `${prefix}${line}`.padEnd(Math.max(safeWidth, line.length + leftPadding)));
|
|
83
|
+
}
|
|
84
|
+
function fullWidthBlankLine(width) {
|
|
85
|
+
return ''.padEnd(Math.max(1, width));
|
|
86
|
+
}
|
|
62
87
|
function ToolMessageText({ text, streaming }) {
|
|
63
88
|
const lines = text.split('\n');
|
|
64
89
|
return _jsx(Box, { flexDirection: "column", children: lines.map((line, index) => {
|
|
@@ -71,29 +96,40 @@ function ToolMessageText({ text, streaming }) {
|
|
|
71
96
|
return _jsxs(Text, { color: theme.muted, children: [indent, _jsx(Text, { color: iconColor, children: icon }), " ", _jsx(Text, { color: theme.purple, children: toolName }), rest] }, `${index}-${line}`);
|
|
72
97
|
}) });
|
|
73
98
|
}
|
|
99
|
+
function MessageView({ message, width }) {
|
|
100
|
+
if (message.role === 'user') {
|
|
101
|
+
return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { backgroundColor: theme.quoteBg, children: fullWidthBlankLine(width) }), _jsx(Text, { color: theme.success, bold: true, backgroundColor: theme.quoteBg, children: ' You asked'.padEnd(width) }), fullWidthLines(message.text, width, 2).map((line, lineIndex) => _jsx(Text, { color: "white", backgroundColor: theme.quoteBg, children: line }, lineIndex)), _jsx(Text, { backgroundColor: theme.quoteBg, children: fullWidthBlankLine(width) })] });
|
|
102
|
+
}
|
|
103
|
+
return _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: message.role === 'assistant' ? theme.purple : message.role === 'tool' ? theme.blue : theme.muted, bold: true, children: message.role === 'assistant' ? 'haze' : message.role === 'tool' ? 'Tool' : 'Info' }), message.role === 'tool'
|
|
104
|
+
? _jsx(ToolMessageText, { text: message.text, streaming: message.streaming })
|
|
105
|
+
: message.role === 'assistant' && !message.streaming
|
|
106
|
+
? _jsx(MarkdownText, { content: message.text })
|
|
107
|
+
: _jsx(Text, { children: message.text })] });
|
|
108
|
+
}
|
|
109
|
+
function messageKey(message, index) {
|
|
110
|
+
return message.id ?? `${index}-${message.role}-${message.text}`;
|
|
111
|
+
}
|
|
74
112
|
function startupProviderInfo(settings) {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
: settings.provider === 'openrouter' || settings.baseURL || settings.apiKey
|
|
83
|
-
? 'OpenRouter'
|
|
84
|
-
: 'OpenRouter (not logged in)';
|
|
113
|
+
const selection = activeModel(settings);
|
|
114
|
+
const model = process.env.HAZE_MODEL ?? selection.model;
|
|
115
|
+
const modelSource = process.env.HAZE_MODEL ? 'HAZE_MODEL env' : settings.model ? 'settings' : 'provider default';
|
|
116
|
+
const baseURL = process.env.OPENAI_BASE_URL ?? selection.provider.url;
|
|
117
|
+
const baseURLSource = process.env.OPENAI_BASE_URL ? 'OPENAI_BASE_URL env' : 'settings';
|
|
118
|
+
const apiKeySource = process.env.OPENAI_API_KEY ? 'OPENAI_API_KEY env' : providerHasKey(settings, selection.provider) ? `provider ${selection.provider.name}` : 'missing';
|
|
119
|
+
const provider = process.env.OPENAI_BASE_URL ? 'OpenAI-compatible custom endpoint' : selection.provider.name;
|
|
85
120
|
return [
|
|
86
121
|
'Provider configuration',
|
|
87
122
|
`- Provider: ${provider}`,
|
|
88
123
|
`- Model: ${model} (${modelSource})`,
|
|
89
124
|
`- Base URL: ${baseURL} (${baseURLSource})`,
|
|
90
|
-
`- API key: ${apiKeySource === 'missing' ? 'not configured;
|
|
125
|
+
`- API key: ${apiKeySource === 'missing' ? 'not configured; local providers may not need one' : `configured via ${apiKeySource}`}`,
|
|
126
|
+
`- Configured providers: ${configuredProviders(settings).length}`,
|
|
91
127
|
].join('\n');
|
|
92
128
|
}
|
|
93
|
-
function ChatScreen({ debug = false, version }) {
|
|
129
|
+
function ChatScreen({ debug = false, version, continueSession = false, noSession = false }) {
|
|
94
130
|
const { exit } = useApp();
|
|
95
131
|
const { stdout } = useStdout();
|
|
96
|
-
const
|
|
132
|
+
const width = stdout.columns ?? process.stdout.columns ?? 80;
|
|
97
133
|
const [messages, setMessages] = useState([
|
|
98
134
|
{ role: 'system', text: 'Welcome to Haze. Use /help for commands.' }
|
|
99
135
|
]);
|
|
@@ -101,14 +137,22 @@ function ChatScreen({ debug = false, version }) {
|
|
|
101
137
|
const conversationRef = useRef([]);
|
|
102
138
|
const lastAssistantTextRef = useRef('');
|
|
103
139
|
const abortControllerRef = useRef(null);
|
|
140
|
+
const sessionRef = useRef(undefined);
|
|
141
|
+
const followUpQueueRef = useRef([]);
|
|
104
142
|
const [inputHistory, setInputHistory] = useState([]);
|
|
105
143
|
const [debugLogs, setDebugLogs] = useState([]);
|
|
106
144
|
const [contextFiles, setContextFiles] = useState([]);
|
|
107
145
|
const [mode, setMode] = useState('chat');
|
|
108
146
|
const [busy, setBusy] = useState(false);
|
|
109
147
|
const [busyLabel, setBusyLabel] = useState('Haze is thinking');
|
|
148
|
+
const [activeGoalStatus, setActiveGoalStatus] = useState();
|
|
149
|
+
const [sessionLabel, setSessionLabel] = useState();
|
|
150
|
+
const [queuedFollowUps, setQueuedFollowUps] = useState([]);
|
|
110
151
|
const [skills, setSkills] = useState([]);
|
|
111
152
|
const [branchName, setBranchName] = useState();
|
|
153
|
+
const [modelProviderFilter, setModelProviderFilter] = useState();
|
|
154
|
+
const [selectedProviderName, setSelectedProviderName] = useState();
|
|
155
|
+
const [providerDraft, setProviderDraft] = useState({});
|
|
112
156
|
useEffect(() => {
|
|
113
157
|
Promise.all([readSettings(), currentBranchName()]).then(([next, branch]) => {
|
|
114
158
|
setSettings(next);
|
|
@@ -122,6 +166,10 @@ function ChatScreen({ debug = false, version }) {
|
|
|
122
166
|
setMessages(m => [...m, { role: 'system', text: startupProviderInfo({}) }]);
|
|
123
167
|
});
|
|
124
168
|
});
|
|
169
|
+
initializeSession().catch(error => {
|
|
170
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
171
|
+
setMessages(m => [...m, { role: 'system', text: `Session disabled: ${text}` }]);
|
|
172
|
+
});
|
|
125
173
|
readInputHistory().then(setInputHistory).catch(() => undefined);
|
|
126
174
|
readContextFiles().then(setContextFiles).catch(() => undefined);
|
|
127
175
|
refreshSkills().catch(() => undefined);
|
|
@@ -142,56 +190,329 @@ function ChatScreen({ debug = false, version }) {
|
|
|
142
190
|
function skillInvocation(value) {
|
|
143
191
|
if (!value.startsWith('/'))
|
|
144
192
|
return undefined;
|
|
145
|
-
const name = value.slice(1).trim();
|
|
146
|
-
if (!name
|
|
193
|
+
const [name, ...args] = value.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
194
|
+
if (!name)
|
|
147
195
|
return undefined;
|
|
148
|
-
|
|
196
|
+
const skill = skills.find(candidate => candidate.name === name);
|
|
197
|
+
return skill ? { skill, args: args.join(' ') } : undefined;
|
|
149
198
|
}
|
|
150
199
|
function debugLog(line) {
|
|
151
200
|
if (!debug)
|
|
152
201
|
return;
|
|
153
202
|
setDebugLogs(current => [...current.slice(-7), line]);
|
|
154
203
|
}
|
|
204
|
+
async function startNewSession(message = 'Started a new session.') {
|
|
205
|
+
if (noSession) {
|
|
206
|
+
sessionRef.current = undefined;
|
|
207
|
+
setSessionLabel('session off');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const session = await createSession({ hazeVersion: version });
|
|
211
|
+
sessionRef.current = session;
|
|
212
|
+
setSessionLabel(session.id);
|
|
213
|
+
setMessages(m => [...m, { role: 'system', text: `${message}\nSession saved: ${session.file}` }]);
|
|
214
|
+
}
|
|
215
|
+
async function initializeSession() {
|
|
216
|
+
if (noSession) {
|
|
217
|
+
setSessionLabel('session off');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (continueSession) {
|
|
221
|
+
const session = await latestSession();
|
|
222
|
+
if (session) {
|
|
223
|
+
const conversation = await restoreConversation(session);
|
|
224
|
+
sessionRef.current = session;
|
|
225
|
+
conversationRef.current = conversation;
|
|
226
|
+
setSessionLabel(session.id);
|
|
227
|
+
setMessages(m => [...m, { role: 'system', text: `Resumed session: ${formatSession(session)}` }, ...displayMessagesFromConversation(conversation)]);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await startNewSession(continueSession ? 'No previous session found. Started a new session.' : 'Started a new session.');
|
|
232
|
+
}
|
|
155
233
|
function clearConversation() {
|
|
156
234
|
conversationRef.current = [];
|
|
157
235
|
lastAssistantTextRef.current = '';
|
|
158
236
|
setMessages([{ role: 'system', text: 'Cleared. The void is productive.' }]);
|
|
237
|
+
const session = sessionRef.current;
|
|
238
|
+
if (session)
|
|
239
|
+
void appendSessionEntry(session, { type: 'event', at: new Date().toISOString(), name: 'clear', text: 'Conversation cleared' }).catch(() => undefined);
|
|
240
|
+
}
|
|
241
|
+
function compactConversation(instructions) {
|
|
242
|
+
const result = compactModelMessages(conversationRef.current, { instructions });
|
|
243
|
+
if (!result.compacted) {
|
|
244
|
+
setMessages(m => [...m, { role: 'system', text: `Compaction skipped: only ${result.keptCount} model messages in context.` }]);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
conversationRef.current = result.messages;
|
|
248
|
+
const session = sessionRef.current;
|
|
249
|
+
if (session) {
|
|
250
|
+
void appendSessionEntry(session, { type: 'event', at: new Date().toISOString(), name: 'compact', text: `Compacted ${result.olderCount} messages; kept ${result.keptCount}.` }).catch(() => undefined);
|
|
251
|
+
void appendSessionEntry(session, { type: 'conversation_snapshot', at: new Date().toISOString(), messages: result.messages }).catch(() => undefined);
|
|
252
|
+
}
|
|
253
|
+
setMessages(m => [...m, { role: 'system', text: `Compacted context: summarized ${result.olderCount} older model messages and kept the last ${result.keptCount}.` }]);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
async function resumeLatestSession() {
|
|
257
|
+
const session = await latestSession();
|
|
258
|
+
if (!session) {
|
|
259
|
+
setMessages(m => [...m, { role: 'system', text: 'No previous session found for this workspace.' }]);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const conversation = await restoreConversation(session);
|
|
263
|
+
sessionRef.current = session;
|
|
264
|
+
conversationRef.current = conversation;
|
|
265
|
+
setSessionLabel(session.id);
|
|
266
|
+
setMessages([{ role: 'system', text: `Resumed session: ${formatSession(session)}` }, ...displayMessagesFromConversation(conversation)]);
|
|
159
267
|
}
|
|
160
268
|
function cancelThinking() {
|
|
161
269
|
if (!busy)
|
|
162
270
|
return;
|
|
163
271
|
abortControllerRef.current?.abort('User pressed Esc.');
|
|
272
|
+
if (followUpQueueRef.current.length > 0) {
|
|
273
|
+
followUpQueueRef.current = [];
|
|
274
|
+
setQueuedFollowUps([]);
|
|
275
|
+
setMessages(m => [...m, { role: 'system', text: 'Cleared queued follow-ups after interrupt.' }]);
|
|
276
|
+
}
|
|
164
277
|
setBusy(false);
|
|
165
278
|
}
|
|
166
|
-
|
|
167
|
-
|
|
279
|
+
function queueFollowUp(value) {
|
|
280
|
+
const trimmed = value.trim();
|
|
281
|
+
if (!trimmed)
|
|
168
282
|
return;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
283
|
+
followUpQueueRef.current = [...followUpQueueRef.current, trimmed];
|
|
284
|
+
setQueuedFollowUps(followUpQueueRef.current);
|
|
285
|
+
setMessages(m => [...m, { role: 'system', text: `Queued follow-up (${followUpQueueRef.current.length}): ${trimmed}` }]);
|
|
286
|
+
}
|
|
287
|
+
function closeInputList() {
|
|
288
|
+
if (mode === 'provider' || mode === 'providerAction' || mode === 'model') {
|
|
172
289
|
setMode('chat');
|
|
173
|
-
|
|
290
|
+
setModelProviderFilter(undefined);
|
|
291
|
+
setSelectedProviderName(undefined);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function providerSuggestions() {
|
|
295
|
+
return [
|
|
296
|
+
...configuredProviders(settings).map(provider => ({
|
|
297
|
+
value: provider.name,
|
|
298
|
+
description: `${provider.url} · ${provider.models.length} model${provider.models.length === 1 ? '' : 's'}`,
|
|
299
|
+
kind: 'provider',
|
|
300
|
+
})),
|
|
301
|
+
{ value: 'add provider', description: 'Add a new OpenAI-compatible provider', kind: 'provider' },
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
function providerActionSuggestions() {
|
|
305
|
+
return [
|
|
306
|
+
{ value: 'use provider', description: 'Set this provider and choose a model', kind: 'provider' },
|
|
307
|
+
{ value: 'add models', description: 'Append comma-separated model names to this provider', kind: 'provider' },
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
function modelSuggestions() {
|
|
311
|
+
const providers = configuredProviders(settings).filter(provider => !modelProviderFilter || provider.name === modelProviderFilter);
|
|
312
|
+
return providers.flatMap(provider => provider.models.map(model => ({
|
|
313
|
+
value: modelProviderFilter ? model : modelSelector(provider, model),
|
|
314
|
+
description: provider.name,
|
|
315
|
+
kind: 'model',
|
|
316
|
+
})));
|
|
317
|
+
}
|
|
318
|
+
async function selectProvider(providerName) {
|
|
319
|
+
if (providerName === 'add provider') {
|
|
320
|
+
setProviderDraft({});
|
|
321
|
+
setMode('providerAddName');
|
|
322
|
+
setMessages(m => [...m, { role: 'system', text: 'Provider name? Example: openrouter, local, lmstudio.' }]);
|
|
174
323
|
return;
|
|
175
324
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
325
|
+
const provider = findProvider(settings, providerName);
|
|
326
|
+
if (!provider) {
|
|
327
|
+
setMessages(m => [...m, { role: 'system', text: `No provider named ${providerName}. Use /provider and choose add provider.` }]);
|
|
328
|
+
setMode('chat');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
setSelectedProviderName(provider.name);
|
|
332
|
+
setMode('providerAction');
|
|
333
|
+
setMessages(m => [...m, { role: 'system', text: `${provider.name}: choose an action.` }]);
|
|
334
|
+
}
|
|
335
|
+
async function useProvider(providerName) {
|
|
336
|
+
const provider = findProvider(settings, providerName);
|
|
337
|
+
if (!provider) {
|
|
338
|
+
setMessages(m => [...m, { role: 'system', text: `No provider named ${providerName}.` }]);
|
|
179
339
|
setMode('chat');
|
|
180
|
-
|
|
340
|
+
setSelectedProviderName(undefined);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const next = await updateSettings({ provider: provider.name });
|
|
344
|
+
setSettings(next);
|
|
345
|
+
setSelectedProviderName(undefined);
|
|
346
|
+
setModelProviderFilter(provider.name);
|
|
347
|
+
setMode('model');
|
|
348
|
+
setMessages(m => [...m, { role: 'system', text: `Provider set to ${provider.name}. Choose a model.` }]);
|
|
349
|
+
}
|
|
350
|
+
async function selectProviderAction(action) {
|
|
351
|
+
if (!selectedProviderName) {
|
|
352
|
+
setMode('provider');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (action === 'use provider') {
|
|
356
|
+
await useProvider(selectedProviderName);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (action === 'add models') {
|
|
360
|
+
setMode('providerAppendModels');
|
|
361
|
+
setMessages(m => [...m, { role: 'system', text: `Comma-separated model names to add to ${selectedProviderName}?` }]);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
setMessages(m => [...m, { role: 'system', text: `Unknown provider action: ${action}` }]);
|
|
365
|
+
}
|
|
366
|
+
async function selectModel(selector) {
|
|
367
|
+
const scopedSelector = modelProviderFilter ? `${modelProviderFilter}:${selector}` : selector;
|
|
368
|
+
const resolved = resolveModelSelector(settings, scopedSelector);
|
|
369
|
+
if (resolved.status === 'ambiguous') {
|
|
370
|
+
setMessages(m => [...m, { role: 'system', text: `Model ${resolved.model} exists on multiple providers: ${resolved.providers.map(provider => modelSelector(provider, resolved.model)).join(', ')}` }]);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (resolved.status === 'missing') {
|
|
374
|
+
setMessages(m => [...m, { role: 'system', text: `No configured model named ${selector}. Use /provider, select a provider, then choose add models.` }]);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const next = await updateSettings({ provider: resolved.provider.name, model: resolved.model });
|
|
378
|
+
setSettings(next);
|
|
379
|
+
setModelProviderFilter(undefined);
|
|
380
|
+
setMode('chat');
|
|
381
|
+
setMessages(m => [...m, { role: 'system', text: `Model set to ${resolved.model} on ${resolved.provider.name}.\n\n${startupProviderInfo(next)}` }]);
|
|
382
|
+
}
|
|
383
|
+
async function appendModelsToProvider(modelsValue) {
|
|
384
|
+
const provider = selectedProviderName ? findProvider(settings, selectedProviderName) : undefined;
|
|
385
|
+
const models = modelsValue.split(',').map(model => model.trim()).filter(Boolean);
|
|
386
|
+
if (!provider) {
|
|
387
|
+
setMessages(m => [...m, { role: 'system', text: 'No provider selected.' }]);
|
|
388
|
+
setMode('chat');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (models.length === 0) {
|
|
392
|
+
setMessages(m => [...m, { role: 'system', text: 'Enter at least one model name.' }]);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const nextProvider = { ...provider, models: [...new Set([...provider.models, ...models])] };
|
|
396
|
+
const next = await updateSettings({ providers: upsertProvider(settings, nextProvider), provider: provider.name });
|
|
397
|
+
setSettings(next);
|
|
398
|
+
setSelectedProviderName(undefined);
|
|
399
|
+
setModelProviderFilter(provider.name);
|
|
400
|
+
setMode('model');
|
|
401
|
+
setMessages(m => [...m, { role: 'system', text: `Added ${models.length} model${models.length === 1 ? '' : 's'} to ${provider.name}. Choose a model.` }]);
|
|
402
|
+
}
|
|
403
|
+
async function finishProviderAdd(modelsValue) {
|
|
404
|
+
const models = modelsValue.split(',').map(model => model.trim()).filter(Boolean);
|
|
405
|
+
if (!providerDraft.name || !providerDraft.url || models.length === 0) {
|
|
406
|
+
setMessages(m => [...m, { role: 'system', text: 'Provider name, URL, and at least one model are required.' }]);
|
|
407
|
+
setMode('chat');
|
|
408
|
+
setProviderDraft({});
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const provider = {
|
|
412
|
+
name: providerDraft.name,
|
|
413
|
+
url: providerDraft.url,
|
|
414
|
+
...(providerDraft.key ? { key: providerDraft.key } : {}),
|
|
415
|
+
models: [...new Set(models)],
|
|
416
|
+
};
|
|
417
|
+
const next = await updateSettings({ providers: upsertProvider(settings, provider), provider: provider.name });
|
|
418
|
+
setSettings(next);
|
|
419
|
+
setProviderDraft({});
|
|
420
|
+
setModelProviderFilter(provider.name);
|
|
421
|
+
setMode('model');
|
|
422
|
+
setMessages(m => [...m, { role: 'system', text: `Added provider ${provider.name}. Choose a model.` }]);
|
|
423
|
+
}
|
|
424
|
+
async function submit(value) {
|
|
425
|
+
if (busy) {
|
|
426
|
+
if (mode === 'chat')
|
|
427
|
+
queueFollowUp(value);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (mode === 'provider') {
|
|
431
|
+
await selectProvider(value);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (mode === 'providerAction') {
|
|
435
|
+
await selectProviderAction(value);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (mode === 'model') {
|
|
439
|
+
await selectModel(value);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (mode === 'providerAddName') {
|
|
443
|
+
const name = value.trim();
|
|
444
|
+
if (!name) {
|
|
445
|
+
setMessages(m => [...m, { role: 'system', text: 'Provider name is required.' }]);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (settings.providers?.some(provider => provider.name === name)) {
|
|
449
|
+
setMessages(m => [...m, { role: 'system', text: `Provider ${name} already exists. Choose a unique name.` }]);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
setProviderDraft({ name });
|
|
453
|
+
setMode('providerAddUrl');
|
|
454
|
+
setMessages(m => [...m, { role: 'system', text: 'OpenAI-compatible base URL? Example: https://openrouter.ai/api/v1 or http://localhost:1234/v1' }]);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (mode === 'providerAddUrl') {
|
|
458
|
+
try {
|
|
459
|
+
new URL(value);
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
setMessages(m => [...m, { role: 'system', text: 'Enter a valid URL, for example http://localhost:1234/v1.' }]);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
setProviderDraft(draft => ({ ...draft, url: value.trim() }));
|
|
466
|
+
setMode('providerAddKey');
|
|
467
|
+
setMessages(m => [...m, { role: 'system', text: 'API key? Leave blank for local/keyless providers.' }]);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (mode === 'providerAddKey') {
|
|
471
|
+
setProviderDraft(draft => ({ ...draft, ...(value.trim() ? { key: value.trim() } : {}) }));
|
|
472
|
+
setMode('providerAddModels');
|
|
473
|
+
setMessages(m => [...m, { role: 'system', text: 'Comma-separated model names? Example: llama3.1, qwen2.5-coder, gpt-4o' }]);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (mode === 'providerAddModels') {
|
|
477
|
+
await finishProviderAdd(value);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (mode === 'providerAppendModels') {
|
|
481
|
+
await appendModelsToProvider(value);
|
|
181
482
|
return;
|
|
182
483
|
}
|
|
183
484
|
const invokedSkill = skillInvocation(value);
|
|
184
485
|
if (invokedSkill) {
|
|
185
|
-
|
|
486
|
+
const argumentText = invokedSkill.args ? `\nUser-provided skill arguments: ${invokedSkill.args}` : '';
|
|
487
|
+
await doAgentTurn(`The user explicitly invoked the "${invokedSkill.skill.name}" skill. Call skill_${invokedSkill.skill.name.replace(/[^a-zA-Z0-9_]/g, '_')} and follow its returned instructions.${argumentText}`, value);
|
|
186
488
|
return;
|
|
187
489
|
}
|
|
188
|
-
const isSkillCreate = /^\/skills? create(?:\s|$)/.test(value);
|
|
490
|
+
const isSkillCreate = /^\/create-skill(?:\s|$)/.test(value) || /^\/skills? create(?:\s|$)/.test(value);
|
|
491
|
+
const skillCreateGoal = isSkillCreate ? createSessionGoal(value) : undefined;
|
|
492
|
+
if (skillCreateGoal) {
|
|
493
|
+
setDebugLogs([]);
|
|
494
|
+
setMessages(m => [...m, { role: 'user', text: value }]);
|
|
495
|
+
const session = sessionRef.current;
|
|
496
|
+
if (session)
|
|
497
|
+
void appendSessionEntry(session, { type: 'ui_message', at: new Date().toISOString(), role: 'user', text: value }).catch(() => undefined);
|
|
498
|
+
setActiveGoalStatus(formatGoalStatus(skillCreateGoal));
|
|
499
|
+
}
|
|
189
500
|
const ctx = {
|
|
190
501
|
settings,
|
|
191
502
|
contextFiles,
|
|
192
503
|
setMode,
|
|
504
|
+
setModelProviderFilter,
|
|
193
505
|
addSystemMessage: text => setMessages(m => [...m, { role: 'system', text }]),
|
|
194
506
|
clearConversation,
|
|
507
|
+
newSession: async () => {
|
|
508
|
+
conversationRef.current = [];
|
|
509
|
+
lastAssistantTextRef.current = '';
|
|
510
|
+
setMessages([{ role: 'system', text: 'Started fresh. The fog parts.' }]);
|
|
511
|
+
await startNewSession('Started a new session.');
|
|
512
|
+
},
|
|
513
|
+
resumeSession: resumeLatestSession,
|
|
514
|
+
sessionInfo: () => sessionRef.current ? formatSession(sessionRef.current) : 'Session persistence is off.',
|
|
515
|
+
compactConversation,
|
|
195
516
|
runAgentTurn: (prompt, displayValue) => doAgentTurn(prompt, displayValue),
|
|
196
517
|
refreshContextFiles: async () => { const files = await readContextFiles().catch(() => contextFiles); setContextFiles(files); return files; },
|
|
197
518
|
updateSettings: async (patch) => {
|
|
@@ -209,12 +530,22 @@ function ChatScreen({ debug = false, version }) {
|
|
|
209
530
|
result = await handleSlashCommand(value, ctx);
|
|
210
531
|
}
|
|
211
532
|
catch (error) {
|
|
533
|
+
if (skillCreateGoal) {
|
|
534
|
+
skillCreateGoal.status = 'blocked';
|
|
535
|
+
skillCreateGoal.blocker = error instanceof Error ? error.message : String(error);
|
|
536
|
+
setActiveGoalStatus(formatGoalStatus(skillCreateGoal));
|
|
537
|
+
}
|
|
212
538
|
const text = error instanceof Error ? error.message : String(error);
|
|
213
539
|
setMessages(m => [...m, { role: 'system', text: `Skill creation failed: ${text}` }]);
|
|
214
540
|
return;
|
|
215
541
|
}
|
|
216
542
|
finally {
|
|
217
543
|
if (isSkillCreate) {
|
|
544
|
+
if (skillCreateGoal?.status === 'active') {
|
|
545
|
+
skillCreateGoal.phase = 'done';
|
|
546
|
+
skillCreateGoal.status = 'complete';
|
|
547
|
+
setActiveGoalStatus(undefined);
|
|
548
|
+
}
|
|
218
549
|
setBusy(false);
|
|
219
550
|
setBusyLabel('Haze is thinking');
|
|
220
551
|
}
|
|
@@ -222,7 +553,7 @@ function ChatScreen({ debug = false, version }) {
|
|
|
222
553
|
if (result === 'exit')
|
|
223
554
|
return exit();
|
|
224
555
|
if (result === 'handled') {
|
|
225
|
-
if (value === '/skill create' || value.startsWith('/skill create ') || value === '/skills create' || value.startsWith('/skills create ') || value.startsWith('/skill remove ') || value.startsWith('/skills remove ')) {
|
|
556
|
+
if (value === '/create-skill' || value.startsWith('/create-skill ') || value === '/skill create' || value.startsWith('/skill create ') || value === '/skills create' || value.startsWith('/skills create ') || value.startsWith('/remove-skill ') || value.startsWith('/skill remove ') || value.startsWith('/skills remove ')) {
|
|
226
557
|
await refreshSkills().catch(() => undefined);
|
|
227
558
|
}
|
|
228
559
|
return;
|
|
@@ -231,63 +562,122 @@ function ChatScreen({ debug = false, version }) {
|
|
|
231
562
|
}
|
|
232
563
|
async function doAgentTurn(value, displayValue) {
|
|
233
564
|
setDebugLogs([]);
|
|
565
|
+
await runSingleAgentTurn(value, displayValue);
|
|
566
|
+
while (followUpQueueRef.current.length > 0) {
|
|
567
|
+
const next = followUpQueueRef.current[0];
|
|
568
|
+
followUpQueueRef.current = followUpQueueRef.current.slice(1);
|
|
569
|
+
setQueuedFollowUps(followUpQueueRef.current);
|
|
570
|
+
setMessages(m => [...m, { role: 'system', text: `Running queued follow-up: ${next}` }]);
|
|
571
|
+
await runSingleAgentTurn(next);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function runSingleAgentTurn(value, displayValue) {
|
|
234
575
|
await runAgentTurn(value, displayValue, contextFiles, {
|
|
235
|
-
addMessage: msg =>
|
|
576
|
+
addMessage: msg => {
|
|
577
|
+
setMessages(m => [...m, msg]);
|
|
578
|
+
const session = sessionRef.current;
|
|
579
|
+
if (session)
|
|
580
|
+
void appendSessionEntry(session, { type: 'ui_message', at: new Date().toISOString(), role: msg.role, text: msg.text }).catch(() => undefined);
|
|
581
|
+
},
|
|
236
582
|
updateMessage: (id, update) => setMessages(m => m.map(msg => msg.id === id ? { ...msg, ...update } : msg)),
|
|
237
|
-
setConversation: msgs => {
|
|
583
|
+
setConversation: msgs => {
|
|
584
|
+
conversationRef.current = msgs;
|
|
585
|
+
const session = sessionRef.current;
|
|
586
|
+
if (session)
|
|
587
|
+
void appendSessionEntry(session, { type: 'conversation_snapshot', at: new Date().toISOString(), messages: msgs }).catch(() => undefined);
|
|
588
|
+
},
|
|
238
589
|
setBusy,
|
|
239
590
|
debugLog,
|
|
240
591
|
getConversation: () => conversationRef.current,
|
|
241
592
|
getLastAssistantText: () => lastAssistantTextRef.current,
|
|
242
593
|
setLastAssistantText: text => { lastAssistantTextRef.current = text; },
|
|
243
594
|
setAbortController: controller => { abortControllerRef.current = controller; },
|
|
595
|
+
setGoalStatus: setActiveGoalStatus,
|
|
596
|
+
compactConversation,
|
|
597
|
+
onEvent: event => {
|
|
598
|
+
const session = sessionRef.current;
|
|
599
|
+
if (session)
|
|
600
|
+
void appendSessionEntry(session, { type: 'event', at: event.at, name: event.type, text: JSON.stringify(event) }).catch(() => undefined);
|
|
601
|
+
},
|
|
244
602
|
});
|
|
245
603
|
}
|
|
246
604
|
const visible = messages.filter(message => !message.hidden);
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
605
|
+
const transcriptItems = visible
|
|
606
|
+
.map((message, index) => ({ key: messageKey(message, index), message }))
|
|
607
|
+
.filter(item => !item.message.streaming);
|
|
608
|
+
const liveMessages = visible.filter(message => message.streaming);
|
|
609
|
+
const activeSelection = activeModel(settings);
|
|
610
|
+
const placeholder = mode === 'provider'
|
|
611
|
+
? 'Choose provider'
|
|
612
|
+
: mode === 'providerAction'
|
|
613
|
+
? 'Choose provider action'
|
|
614
|
+
: mode === 'model'
|
|
615
|
+
? 'Choose model'
|
|
616
|
+
: mode === 'providerAddName'
|
|
617
|
+
? 'Provider name'
|
|
618
|
+
: mode === 'providerAddUrl'
|
|
619
|
+
? 'https://example.com/v1'
|
|
620
|
+
: mode === 'providerAddKey'
|
|
621
|
+
? 'API key, or blank for local'
|
|
622
|
+
: mode === 'providerAddModels' || mode === 'providerAppendModels'
|
|
623
|
+
? 'model-a, model-b'
|
|
624
|
+
: busy ? 'Queue a follow-up, or Esc to interrupt' : 'Ask Haze to help build your app';
|
|
625
|
+
const activeModelName = `${activeSelection.provider.name}:${process.env.HAZE_MODEL ?? activeSelection.model}`;
|
|
626
|
+
const hasLogin = Boolean(process.env.OPENAI_API_KEY ?? settings.apiKey ?? activeSelection.provider.key) || activeSelection.provider.name !== DEFAULT_PROVIDER_NAME;
|
|
627
|
+
const hasChosenModel = Boolean(process.env.HAZE_MODEL ?? settings.model ?? activeSelection.model);
|
|
251
628
|
const headerSubtitle = hasLogin && hasChosenModel
|
|
252
629
|
? [
|
|
253
630
|
'A minimal LLM harness for growing your own workflows while you work.',
|
|
254
631
|
'',
|
|
255
632
|
'Start with simple chat, then teach Haze your habits with skills:',
|
|
256
|
-
'/skill
|
|
633
|
+
'/create-skill review my branch against main — tiny spell, useful goblin.',
|
|
257
634
|
'',
|
|
258
635
|
'The most adaptive workflow is the one you shape as you go.',
|
|
259
636
|
'',
|
|
260
637
|
'Guardrails are light: Haze lets the LLM work from the terminal almost like you,',
|
|
261
638
|
'while trying to stay scoped to this project.',
|
|
262
639
|
].join('\n')
|
|
263
|
-
: 'First things first: run /
|
|
640
|
+
: 'First things first: run /provider to choose or add a provider, then select a model.';
|
|
264
641
|
const workspaceLabel = `${process.cwd()}${branchName ? ` (${branchName})` : ''}`;
|
|
265
642
|
const toolsUsed = toolCallCount(messages);
|
|
266
643
|
const estimatedTokens = estimateConversationTokens(messages);
|
|
267
|
-
const statusDetailLabel = `${conversationRef.current.length} messages / ${toolsUsed} tool call${toolsUsed === 1 ? '' : 's'} / ↑ ~${formatTokenCount(estimatedTokens.input)} ↓ ~${formatTokenCount(estimatedTokens.output)} / ${skills.length} skill${skills.length === 1 ? '' : 's'}`;
|
|
268
|
-
const
|
|
644
|
+
const statusDetailLabel = `${conversationRef.current.length} messages / ${toolsUsed} tool call${toolsUsed === 1 ? '' : 's'} / ↑ ~${formatTokenCount(estimatedTokens.input)} ↓ ~${formatTokenCount(estimatedTokens.output)} / ${skills.length} skill${skills.length === 1 ? '' : 's'}${sessionLabel ? ` / ${sessionLabel}` : ''}`;
|
|
645
|
+
const goalText = activeGoalStatus?.replace(/^Goal:\s*/, '');
|
|
646
|
+
const [rawGoalRequest, ...goalStatusParts] = goalText?.split(' · ') ?? [];
|
|
647
|
+
const goalRequest = truncateWithEllipsis(rawGoalRequest ?? '', 120);
|
|
648
|
+
const goalStatusText = goalStatusParts.join(' · ');
|
|
649
|
+
const inputSuggestions = mode === 'provider' ? providerSuggestions() : mode === 'providerAction' ? providerActionSuggestions() : mode === 'model' ? modelSuggestions() : mode === 'chat' ? [
|
|
269
650
|
{ value: '/help', description: 'Show commands', kind: 'command' },
|
|
270
|
-
{ value: '/
|
|
651
|
+
{ value: '/provider', description: 'Choose a provider', kind: 'command' },
|
|
271
652
|
{ value: '/model', description: 'Choose a model', kind: 'command' },
|
|
272
653
|
{ value: '/settings', description: 'Show provider, model, API key, and context status', kind: 'command' },
|
|
273
|
-
{ value: '/skill
|
|
274
|
-
{ value: '/
|
|
275
|
-
{ value: '/skill
|
|
276
|
-
{ value: '/skill
|
|
277
|
-
{ value: '/skill
|
|
654
|
+
{ value: '/create-skill ', description: 'Create a Markdown skill', kind: 'command' },
|
|
655
|
+
{ value: '/list-skills', description: 'List installed skills', kind: 'command' },
|
|
656
|
+
{ value: '/skill-info ', description: 'Show details for a skill', kind: 'command' },
|
|
657
|
+
{ value: '/validate-skill ', description: 'Validate a skill', kind: 'command' },
|
|
658
|
+
{ value: '/remove-skill ', description: 'Remove a skill with --yes', kind: 'command' },
|
|
278
659
|
{ value: '/init', description: 'Create or update AGENTS.md project instructions', kind: 'command' },
|
|
660
|
+
{ value: '/session', description: 'Show current session path', kind: 'command' },
|
|
661
|
+
{ value: '/resume', description: 'Resume latest session for this workspace', kind: 'command' },
|
|
662
|
+
{ value: '/new', description: 'Start a new session', kind: 'command' },
|
|
663
|
+
{ value: '/compact ', description: 'Summarize older context and keep recent messages', kind: 'command' },
|
|
279
664
|
{ value: '/clear', description: 'Clear conversation history', kind: 'command' },
|
|
280
665
|
{ value: '/exit', description: 'Exit Haze', kind: 'command' },
|
|
281
666
|
{ value: '/quit', description: 'Exit Haze', kind: 'command' },
|
|
282
667
|
...skills.map(skill => ({ value: `/${skill.name}`, description: skill.description, kind: 'skill' })),
|
|
283
668
|
] : [];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
669
|
+
const staticItems = [
|
|
670
|
+
{ kind: 'header', key: `header-${activeModelName}-${hasLogin}-${hasChosenModel}`, subtitle: headerSubtitle },
|
|
671
|
+
...transcriptItems.map(item => ({ kind: 'message', ...item })),
|
|
672
|
+
];
|
|
673
|
+
return _jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: item => item.kind === 'header'
|
|
674
|
+
? _jsx(Header, { subtitle: item.subtitle, version: version }, item.key)
|
|
675
|
+
: _jsx(MessageView, { message: item.message, width: width }, item.key) }), liveMessages.length > 0 && _jsx(Box, { flexDirection: "column", flexShrink: 0, children: liveMessages.map((message, index) => _jsx(MessageView, { message: message, width: width }, messageKey(message, index))) }), debug && debugLogs.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, marginBottom: 1, borderStyle: "round", borderColor: theme.muted, paddingX: 1, children: [_jsx(Text, { color: theme.muted, bold: true, children: "Debug" }), debugLogs.map((line, index) => _jsxs(Text, { color: theme.muted, children: ["\u2022 ", line] }, index))] }), queuedFollowUps.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, marginBottom: 1, children: [_jsx(Text, { color: theme.muted, children: "Queued follow-ups:" }), queuedFollowUps.map((item, index) => _jsxs(Text, { color: theme.muted, dimColor: true, children: [" ", index + 1, ". ", item] }, `${index}-${item}`))] }), busy && _jsx(Box, { flexShrink: 0, marginBottom: 1, children: _jsxs(Text, { children: [_jsxs(Text, { color: theme.orange, bold: true, children: [_jsx(Spinner, { type: "dots" }), " ", busyLabel] }), _jsx(Text, { color: theme.muted, dimColor: true, children: " \u00B7 type to queue follow-up \u00B7 esc to interrupt" })] }) }), goalText && _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: theme.blue, bold: true, children: "Goal:" }), _jsxs(Text, { color: "white", children: [" ", goalRequest] }), goalStatusText ? _jsxs(Text, { color: theme.orange, children: [" \u00B7 ", goalStatusText] }) : null] }) }), _jsx(Box, { borderStyle: "round", borderColor: theme.deepPurple, paddingX: 1, flexShrink: 0, children: _jsx(Box, { flexGrow: 1, minWidth: 0, children: _jsx(TextInput, { placeholder: placeholder, disabled: busy && mode !== 'chat', mask: mode === 'providerAddKey', historyItems: inputHistory, recordHistory: mode === 'chat', suggestions: inputSuggestions, suggestionMode: mode === 'provider' || mode === 'providerAction' || mode === 'model' ? 'always' : 'slash', submitOnEmpty: mode === 'providerAddKey', onHistoryAdd: persistInputHistory, onCancel: cancelThinking, onEscape: closeInputList, onSubmit: submit }) }) }), _jsxs(Box, { flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", flexShrink: 1, minWidth: 0, children: [_jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: workspaceLabel }), _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-end", children: statusDetailLabel })] }), _jsx(Box, { flexShrink: 0, marginLeft: 2, children: _jsx(Text, { color: theme.muted, dimColor: true, wrap: "truncate-start", children: activeModelName }) })] })] });
|
|
289
676
|
}
|
|
290
677
|
export async function chatCommand(options = {}) {
|
|
291
|
-
|
|
678
|
+
if (process.stdout.isTTY) {
|
|
679
|
+
process.stdout.write('\u001B[2J\u001B[3J\u001B[H');
|
|
680
|
+
}
|
|
681
|
+
const app = render(_jsx(ChatScreen, { debug: options.debug, version: options.version, continueSession: options.continueSession, noSession: options.noSession }));
|
|
292
682
|
await app.waitUntilExit();
|
|
293
683
|
}
|