@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +100 -34
  3. package/dist/cli/commands/chat.d.ts +3 -1
  4. package/dist/cli/commands/chat.js +500 -56
  5. package/dist/cli/commands/commands.d.ts +5 -0
  6. package/dist/cli/commands/commands.js +114 -29
  7. package/dist/cli/commands/formatters.js +32 -2
  8. package/dist/cli/commands/streaming.d.ts +6 -1
  9. package/dist/cli/commands/streaming.js +316 -98
  10. package/dist/cli/index.js +5 -2
  11. package/dist/config/inputHistory.js +8 -0
  12. package/dist/config/providers.d.ts +26 -0
  13. package/dist/config/providers.js +88 -0
  14. package/dist/config/settings.d.ts +9 -2
  15. package/dist/core/agent/compaction.d.ts +13 -0
  16. package/dist/core/agent/compaction.js +34 -0
  17. package/dist/core/agent/errors.d.ts +3 -0
  18. package/dist/core/agent/errors.js +13 -0
  19. package/dist/core/agent/events.d.ts +58 -0
  20. package/dist/core/agent/events.js +3 -0
  21. package/dist/core/goal/completionPolicy.d.ts +28 -0
  22. package/dist/core/goal/completionPolicy.js +84 -0
  23. package/dist/core/goal/requestClassifier.d.ts +6 -0
  24. package/dist/core/goal/requestClassifier.js +31 -0
  25. package/dist/core/goal/sessionGoal.d.ts +30 -0
  26. package/dist/core/goal/sessionGoal.js +88 -0
  27. package/dist/core/session/sessionStore.d.ts +37 -0
  28. package/dist/core/session/sessionStore.js +59 -0
  29. package/dist/core/subagent/subagentRunner.d.ts +33 -0
  30. package/dist/core/subagent/subagentRunner.js +140 -0
  31. package/dist/llm/client.d.ts +1 -1
  32. package/dist/llm/client.js +6 -6
  33. package/dist/llm/hazeTools.d.ts +86 -0
  34. package/dist/llm/hazeTools.js +313 -93
  35. package/dist/llm/initPrompt.js +6 -4
  36. package/dist/llm/systemPrompt.js +11 -7
  37. package/dist/skills/builder/SkillBuilder.d.ts +6 -0
  38. package/dist/skills/builder/SkillBuilder.js +146 -24
  39. package/dist/ui/components/ErrorView.d.ts +2 -1
  40. package/dist/ui/components/Header.d.ts +2 -1
  41. package/dist/ui/components/Header.js +1 -11
  42. package/dist/ui/components/MarkdownText.d.ts +2 -1
  43. package/dist/ui/components/TextInput.d.ts +7 -3
  44. package/dist/ui/components/TextInput.js +112 -27
  45. package/dist/ui/theme.d.ts +3 -0
  46. package/dist/ui/theme.js +4 -1
  47. package/package.json +8 -8
@@ -1,12 +1,13 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
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 model = process.env.HAZE_MODEL ?? settings.model ?? 'x-ai/grok-build-0.1';
76
- const modelSource = process.env.HAZE_MODEL ? 'HAZE_MODEL env' : settings.model ? 'settings' : 'default';
77
- const baseURL = process.env.OPENAI_BASE_URL ?? settings.baseURL ?? 'https://openrouter.ai/api/v1';
78
- const baseURLSource = process.env.OPENAI_BASE_URL ? 'OPENAI_BASE_URL env' : settings.baseURL ? 'settings' : 'default';
79
- const apiKeySource = process.env.OPENAI_API_KEY ? 'OPENAI_API_KEY env' : settings.apiKey ? '~/.haze/settings.json' : 'missing';
80
- const provider = process.env.OPENAI_BASE_URL
81
- ? 'OpenAI-compatible custom endpoint'
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; run /login or set OPENAI_API_KEY' : `configured via ${apiKeySource}`}`,
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 height = stdout.rows ?? process.stdout.rows ?? 24;
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 || name.includes(' '))
213
+ const [name, ...args] = value.slice(1).trim().split(/\s+/).filter(Boolean);
214
+ if (!name)
147
215
  return undefined;
148
- return skills.find(skill => skill.name === name);
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
- async function submit(value) {
167
- if (busy)
302
+ function queueFollowUp(value) {
303
+ const trimmed = value.trim();
304
+ if (!trimmed)
168
305
  return;
169
- if (mode === 'apiKey') {
170
- const next = await updateSettings({ provider: 'openrouter', apiKey: value, baseURL: 'https://openrouter.ai/api/v1' });
171
- setSettings(next);
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
- setMessages(m => [...m, { role: 'system', text: `OpenRouter login saved to ~/.haze/settings.json. Security theatre completed.\n\n${startupProviderInfo(next)}` }]);
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
- if (mode === 'model') {
177
- const next = await updateSettings({ model: value });
178
- setSettings(next);
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
- setMessages(m => [...m, { role: 'system', text: `Model set to ${value}.\n\n${startupProviderInfo(next)}` }]);
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
- await doAgentTurn(`The user explicitly invoked the "${invokedSkill.name}" skill. Call skill_${invokedSkill.name.replace(/[^a-zA-Z0-9_]/g, '_')} and follow its returned instructions.`, value);
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 => setMessages(m => [...m, msg]),
236
- updateMessage: (id, update) => setMessages(m => m.map(msg => msg.id === id ? { ...msg, ...update } : msg)),
237
- setConversation: msgs => { conversationRef.current = msgs; },
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 placeholder = mode === 'apiKey' ? 'OpenRouter API key' : mode === 'model' ? 'x-ai/grok-build-0.1' : busy ? 'Thinking, allegedly' : 'Ask Haze to help build your app';
248
- const activeModelName = process.env.HAZE_MODEL ?? settings.model ?? 'x-ai/grok-build-0.1';
249
- const hasLogin = Boolean(process.env.OPENAI_API_KEY ?? settings.apiKey);
250
- const hasChosenModel = Boolean(process.env.HAZE_MODEL ?? settings.model);
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 create review my branch against main — tiny spell, useful goblin.',
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 /login to add your API key, then /model x-ai/grok-build-0.1 to choose a model.';
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 toolsUsed = toolCallCount(messages);
266
- 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 slashSuggestions = mode === 'chat' ? [
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: '/login', description: 'Save an OpenRouter API key', kind: 'command' },
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 create ', description: 'Create a Markdown skill', kind: 'command' },
274
- { value: '/skill list', description: 'List installed skills', kind: 'command' },
275
- { value: '/skill info ', description: 'Show details for a skill', kind: 'command' },
276
- { value: '/skill validate ', description: 'Validate a skill', kind: 'command' },
277
- { value: '/skill remove ', description: 'Remove a skill with --yes', kind: 'command' },
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
- return _jsxs(Box, { flexDirection: "column", minHeight: height, children: [_jsx(Box, { flexShrink: 0, children: _jsx(Header, { subtitle: headerSubtitle, version: version }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visible.map((message, index) => _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: message.role === 'user' ? theme.purple : message.role === 'assistant' ? theme.success : message.role === 'tool' ? theme.blue : theme.muted, bold: true, children: message.role === 'user' ? 'You' : message.role === 'assistant' ? 'Haze' : message.role === 'tool' ? 'Tool' : 'Info' }), message.role === 'tool'
285
- ? _jsx(ToolMessageText, { text: message.text, streaming: message.streaming })
286
- : message.role === 'assistant' && !message.streaming
287
- ? _jsx(MarkdownText, { content: message.text })
288
- : _jsx(Text, { children: message.text })] }, 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))] }), busy && _jsx(Box, { flexShrink: 0, marginBottom: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Spinner, { type: "dots" }), " ", busyLabel, _jsx(Text, { dimColor: true, children: " \u00B7 esc to interrupt" })] }) }), _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, mask: mode === 'apiKey', historyItems: inputHistory, recordHistory: mode === 'chat', suggestions: slashSuggestions, onHistoryAdd: persistInputHistory, onCancel: cancelThinking, 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 }) })] })] });
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
- const app = render(_jsx(ChatScreen, { debug: options.debug }));
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
  }