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