@denizokcu/haze 0.0.1 → 0.0.2
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 +14 -0
- package/README.md +114 -69
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +203 -11
- package/dist/cli/commands/commands.js +130 -6
- package/dist/cli/commands/formatters.d.ts +1 -0
- package/dist/cli/commands/formatters.js +18 -1
- package/dist/cli/commands/skills.d.ts +1 -1
- package/dist/cli/commands/skills.js +8 -5
- package/dist/cli/commands/streaming.d.ts +2 -0
- package/dist/cli/commands/streaming.js +424 -39
- package/dist/cli/index.js +1 -11
- package/dist/config/paths.d.ts +0 -1
- package/dist/config/paths.js +0 -1
- package/dist/llm/client.js +1 -1
- package/dist/llm/hazeTools.d.ts +32 -0
- package/dist/llm/hazeTools.js +136 -26
- package/dist/llm/initPrompt.js +2 -2
- package/dist/llm/systemPrompt.js +23 -9
- package/dist/skills/SkillLoader.d.ts +12 -2
- package/dist/skills/SkillLoader.js +64 -18
- package/dist/skills/SkillRegistry.d.ts +1 -5
- package/dist/skills/SkillRegistry.js +10 -21
- package/dist/skills/builder/SkillBuilder.d.ts +25 -1
- package/dist/skills/builder/SkillBuilder.js +169 -20
- package/dist/skills/skillTools.d.ts +20 -0
- package/dist/skills/skillTools.js +25 -0
- package/dist/skills/types.d.ts +12 -51
- package/dist/ui/components/Header.d.ts +2 -1
- package/dist/ui/components/Header.js +12 -2
- package/dist/ui/components/TextInput.d.ts +8 -1
- package/dist/ui/components/TextInput.js +29 -14
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.js +1 -0
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.js +10 -6
- package/examples/skills/files/SKILL.md +16 -0
- package/examples/skills/files/examples/file-editing.md +3 -0
- package/package.json +2 -2
- package/dist/skills/installer/SkillInstaller.d.ts +0 -1
- package/dist/skills/installer/SkillInstaller.js +0 -48
- package/dist/skills/manifestSchema.d.ts +0 -31
- package/dist/skills/manifestSchema.js +0 -23
- package/dist/tools/ToolExecutor.d.ts +0 -3
- package/dist/tools/ToolExecutor.js +0 -15
- package/dist/tools/types.d.ts +0 -9
- package/dist/tools/types.js +0 -1
- package/examples/skills/files/prompts/file_tasks.md +0 -1
- package/examples/skills/files/skill.yaml +0 -28
- package/examples/skills/files/tools/list_files.ts +0 -21
- package/examples/skills/files/tools/read_file.ts +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.0.2 - 2026-06-01
|
|
6
|
+
|
|
7
|
+
- Reworked skills into Markdown-first workflows stored in `~/.haze/skills/<name>/SKILL.md`.
|
|
8
|
+
- Added LLM-generated `/skill create <description>` for creating workflow skills from natural language.
|
|
9
|
+
- Exposed installed skills as model-selectable `skill_*` tools and slash-invokable commands.
|
|
10
|
+
- Added slash-command and skill autocomplete with `Tab` completion.
|
|
11
|
+
- Grouped tool calls into compact per-turn activity blocks.
|
|
12
|
+
- Added `listFiles` cursor pagination for large recursive listings.
|
|
13
|
+
- Refined startup/onboarding UI with ASCII logo, status bar, model/workspace details, and clearer setup guidance.
|
|
14
|
+
- Updated README for the minimal LLM harness and adaptive skill workflow.
|
|
15
|
+
- Removed old YAML/executable skill tooling.
|
|
16
|
+
|
|
3
17
|
## 0.0.1 - 2026-05-31
|
|
4
18
|
|
|
5
19
|
Initial public release.
|
package/README.md
CHANGED
|
@@ -1,67 +1,114 @@
|
|
|
1
1
|
# Haze
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A minimal LLM harness for your terminal.
|
|
4
|
+
|
|
5
|
+
Haze gives an AI model a small set of transparent local tools — read files, edit files, write files, list files, and run commands — then gets out of the way. Start with chat. Build your workflows as you work. Teach Haze with Markdown skills when a pattern repeats. Tiny spell, useful goblin.
|
|
6
|
+
|
|
7
|
+
MVP scope: Haze currently uses OpenRouter only. More providers are on the roadmap after the goblin learns to hold a spoon safely.
|
|
8
|
+
|
|
9
|
+
```txt
|
|
10
|
+
_
|
|
11
|
+
| |
|
|
12
|
+
| |__ __ _ _______
|
|
13
|
+
| '_ \ / _` |_ / _ \
|
|
14
|
+
| | | | (_| |/ / __/
|
|
15
|
+
|_| |_|\__,_/___\___|
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Haze keeps guardrails light. The LLM can work from the terminal with freedoms close to yours, while trying to stay scoped to the current project. Watch the tool calls. Keep your hands near the wheel. Progress.
|
|
4
19
|
|
|
5
20
|
## Install
|
|
6
21
|
|
|
7
22
|
```bash
|
|
8
23
|
npm install -g @denizokcu/haze
|
|
24
|
+
haze
|
|
9
25
|
```
|
|
10
26
|
|
|
11
|
-
|
|
27
|
+
First run inside Haze, do both steps:
|
|
12
28
|
|
|
13
|
-
```
|
|
14
|
-
|
|
29
|
+
```txt
|
|
30
|
+
/login
|
|
31
|
+
/model x-ai/grok-build-0.1
|
|
15
32
|
```
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
`/login` saves your API key. `/model` saves the model Haze should use. The recommended MVP model is `x-ai/grok-build-0.1`.
|
|
35
|
+
|
|
36
|
+
Or use environment variables:
|
|
18
37
|
|
|
19
38
|
```bash
|
|
20
|
-
|
|
21
|
-
|
|
39
|
+
export OPENAI_API_KEY=... # your OpenRouter API key
|
|
40
|
+
export HAZE_MODEL=x-ai/grok-build-0.1
|
|
22
41
|
```
|
|
23
42
|
|
|
24
|
-
|
|
43
|
+
Saved settings live in `~/.haze/settings.json`. The current MVP experience is documented around OpenRouter; more provider docs are future work.
|
|
25
44
|
|
|
26
|
-
|
|
45
|
+
## Get productive immediately
|
|
46
|
+
|
|
47
|
+
Open a project and ask for work:
|
|
27
48
|
|
|
28
49
|
```txt
|
|
29
|
-
|
|
30
|
-
/model openai/gpt-4o-mini
|
|
50
|
+
create a calculator in calc-app in ruby with add subtract multiply divide
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
Haze will inspect, write files, run commands, and show compact tool activity inline.
|
|
54
|
+
|
|
55
|
+
Use `/` to discover commands and skills. `Tab` completes the top suggestion.
|
|
56
|
+
|
|
57
|
+
Useful starters:
|
|
34
58
|
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"model": "openai/gpt-4o-mini"
|
|
41
|
-
}
|
|
59
|
+
```txt
|
|
60
|
+
/init
|
|
61
|
+
/skill create review my current branch against main like a senior engineer
|
|
62
|
+
/skill create prepare clean git commits from my uncommitted changes
|
|
63
|
+
/skill create implement small features with tests and a concise summary
|
|
42
64
|
```
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
`/init` creates or updates `AGENTS.md` so future sessions understand the project.
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
## Skills: your workflows, grown while working
|
|
69
|
+
|
|
70
|
+
Skills are Markdown workflows stored in `~/.haze/skills`.
|
|
71
|
+
|
|
72
|
+
When you notice yourself asking for the same kind of work, make it a skill:
|
|
73
|
+
|
|
74
|
+
```txt
|
|
75
|
+
/skill create review the diff between my current branch and main, focusing on bugs, tests, DRY and KISS
|
|
50
76
|
```
|
|
51
77
|
|
|
52
|
-
|
|
78
|
+
Haze uses the model to create:
|
|
53
79
|
|
|
54
|
-
```
|
|
55
|
-
haze
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
```txt
|
|
81
|
+
~/.haze/skills/<skill-name>/SKILL.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A skill is just Markdown with frontmatter:
|
|
85
|
+
|
|
86
|
+
```md
|
|
87
|
+
---
|
|
88
|
+
name: code-review-diff-main
|
|
89
|
+
description: Use when the user asks for a code review of the current branch against main.
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
# Goal
|
|
93
|
+
|
|
94
|
+
Review the actual change and return useful, evidence-based feedback.
|
|
95
|
+
|
|
96
|
+
# Procedure
|
|
97
|
+
|
|
98
|
+
Inspect branch state, changed files, staged and unstaged diffs, then review incrementally.
|
|
62
99
|
```
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
Installed skills appear as slash commands like:
|
|
102
|
+
|
|
103
|
+
```txt
|
|
104
|
+
/code-review-diff-main
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
They are also exposed to the model as `skill_*` tools. The skill does not execute code; it gives Haze a workflow to follow.
|
|
108
|
+
|
|
109
|
+
This is the trick: do normal work, notice friction, create a skill, keep going. Your workflow adapts instead of asking you to adapt to the tool. Rude, but in a good way.
|
|
110
|
+
|
|
111
|
+
## Commands
|
|
65
112
|
|
|
66
113
|
```txt
|
|
67
114
|
/help
|
|
@@ -72,70 +119,68 @@ Chat commands:
|
|
|
72
119
|
/init
|
|
73
120
|
/clear
|
|
74
121
|
/exit
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`/init` explores the current workspace using `.gitignore`-aware tools and creates or updates an `AGENTS.md` file with project instructions for future Haze sessions.
|
|
78
122
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
/skill create <description>
|
|
124
|
+
/skill list
|
|
125
|
+
/skill info <name>
|
|
126
|
+
/skill validate <name-or-dir>
|
|
127
|
+
/skill remove <name> --yes
|
|
128
|
+
```
|
|
85
129
|
|
|
86
|
-
|
|
130
|
+
`/skills ...` also works as an alias for `/skill ...`.
|
|
87
131
|
|
|
88
132
|
## Agent tools
|
|
89
133
|
|
|
90
|
-
Haze exposes a small toolset
|
|
134
|
+
Haze exposes a deliberately small toolset:
|
|
91
135
|
|
|
92
|
-
- `listFiles` — structured
|
|
136
|
+
- `listFiles` — structured discovery, recursive with cursor pagination when needed.
|
|
93
137
|
- `readFile` — read UTF-8 files with optional line ranges.
|
|
94
138
|
- `editFile` — exact unique text replacements.
|
|
95
|
-
- `replaceLines` —
|
|
96
|
-
- `writeFile` — create
|
|
97
|
-
- `bash` — run
|
|
139
|
+
- `replaceLines` — line-range edits when exact replacements are awkward.
|
|
140
|
+
- `writeFile` — create files and parent directories.
|
|
141
|
+
- `bash` — run tests, builds, git commands, and inspections.
|
|
142
|
+
- `skill_*` — load Markdown skill instructions on demand.
|
|
98
143
|
|
|
99
|
-
Tool calls are
|
|
144
|
+
Tool calls are grouped in the transcript so you can see what happened without reading a novella.
|
|
100
145
|
|
|
101
146
|
## Context files
|
|
102
147
|
|
|
103
|
-
Haze loads project instructions from
|
|
148
|
+
Haze loads project instructions from:
|
|
104
149
|
|
|
105
150
|
- `~/.haze/AGENTS.md`
|
|
106
151
|
- `~/.haze/CLAUDE.md`
|
|
107
|
-
- `AGENTS.md` files
|
|
108
|
-
- `CLAUDE.md` files
|
|
152
|
+
- `AGENTS.md` files from filesystem root to the current workspace
|
|
153
|
+
- `CLAUDE.md` files from filesystem root to the current workspace
|
|
109
154
|
|
|
110
|
-
Use `AGENTS.md` for
|
|
155
|
+
Use `AGENTS.md` for project conventions, commands, architecture notes, and things future-you does not want to re-explain.
|
|
111
156
|
|
|
112
157
|
## Safety model
|
|
113
158
|
|
|
114
159
|
- File tools are restricted to the current workspace.
|
|
115
160
|
- File tools follow `.gitignore` by default.
|
|
116
|
-
- Ignored files
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
## Skills
|
|
121
|
-
|
|
122
|
-
Default skill locations:
|
|
123
|
-
|
|
124
|
-
- `~/.haze/skills/`
|
|
125
|
-
- `./.haze/skills/` local overrides global
|
|
161
|
+
- Ignored files require an explicit override.
|
|
162
|
+
- Bash mutations are discouraged by the tool contract.
|
|
163
|
+
- Destructive actions should require explicit user confirmation.
|
|
164
|
+
- Haze is powerful enough to help and dumb enough to deserve supervision. Ideal software, basically.
|
|
126
165
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
## Development
|
|
166
|
+
## Local development
|
|
130
167
|
|
|
131
168
|
```bash
|
|
132
169
|
npm install
|
|
170
|
+
npm run dev
|
|
133
171
|
npm run typecheck
|
|
172
|
+
npm test
|
|
173
|
+
npm run lint
|
|
134
174
|
npm run build
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Package check:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
135
180
|
npm pack --dry-run
|
|
136
181
|
```
|
|
137
182
|
|
|
138
|
-
The npm package
|
|
183
|
+
The npm package ships `bin`, `dist`, docs, license, changelog, and examples.
|
|
139
184
|
|
|
140
185
|
## Release
|
|
141
186
|
|
|
@@ -143,7 +188,7 @@ The npm package intentionally ships only `bin`, `dist`, `README.md`, `LICENSE`,
|
|
|
143
188
|
npm run typecheck
|
|
144
189
|
npm run build
|
|
145
190
|
npm pack --dry-run
|
|
146
|
-
git tag
|
|
191
|
+
git tag vX.Y.Z
|
|
147
192
|
git push origin main --tags
|
|
148
193
|
npm publish --access public
|
|
149
194
|
```
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
3
5
|
import { Box, render, Text, useApp, useStdout } from 'ink';
|
|
4
6
|
import Spinner from 'ink-spinner';
|
|
5
7
|
import { readContextFiles } from '../../config/contextFiles.js';
|
|
@@ -11,29 +13,140 @@ import { MarkdownText } from '../../ui/components/MarkdownText.js';
|
|
|
11
13
|
import { theme } from '../../ui/theme.js';
|
|
12
14
|
import { handleSlashCommand } from './commands.js';
|
|
13
15
|
import { runAgentTurn } from './streaming.js';
|
|
14
|
-
|
|
16
|
+
import { loadSkillRegistry } from '../../skills/SkillRegistry.js';
|
|
17
|
+
const execFile = promisify(execFileCallback);
|
|
18
|
+
async function currentBranchName() {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execFile('git', ['branch', '--show-current'], { cwd: process.cwd() });
|
|
21
|
+
return stdout.trim() || undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function toolCallCount(messages) {
|
|
28
|
+
return messages.reduce((total, message) => {
|
|
29
|
+
if (message.role !== 'tool')
|
|
30
|
+
return total;
|
|
31
|
+
const headerCount = /Tools: (\d+) calls?/.exec(message.text)?.[1];
|
|
32
|
+
if (headerCount)
|
|
33
|
+
return total + Number(headerCount);
|
|
34
|
+
const rows = message.text.split('\n').filter(line => /^\s+[✓✗…]\s/.test(line));
|
|
35
|
+
return total + rows.reduce((rowTotal, row) => rowTotal + Number(/×(\d+)/.exec(row)?.[1] ?? 1), 0);
|
|
36
|
+
}, 0);
|
|
37
|
+
}
|
|
38
|
+
function estimateTokens(text) {
|
|
39
|
+
return Math.ceil(text.length / 4);
|
|
40
|
+
}
|
|
41
|
+
function formatTokenCount(tokens) {
|
|
42
|
+
if (tokens >= 1_000_000)
|
|
43
|
+
return `${(tokens / 1_000_000).toFixed(tokens >= 10_000_000 ? 0 : 1).replace(/\.0$/, '')}m`;
|
|
44
|
+
if (tokens >= 1_000)
|
|
45
|
+
return `${(tokens / 1_000).toFixed(tokens >= 10_000 ? 0 : 1).replace(/\.0$/, '')}k`;
|
|
46
|
+
return String(tokens);
|
|
47
|
+
}
|
|
48
|
+
function estimateConversationTokens(messages) {
|
|
49
|
+
const inputText = messages
|
|
50
|
+
.filter(message => message.role === 'user' || message.role === 'tool')
|
|
51
|
+
.map(message => message.text)
|
|
52
|
+
.join('\n');
|
|
53
|
+
const outputText = messages
|
|
54
|
+
.filter(message => message.role === 'assistant')
|
|
55
|
+
.map(message => message.text)
|
|
56
|
+
.join('\n');
|
|
57
|
+
return {
|
|
58
|
+
input: estimateTokens(inputText),
|
|
59
|
+
output: estimateTokens(outputText),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function ToolMessageText({ text, streaming }) {
|
|
63
|
+
const lines = text.split('\n');
|
|
64
|
+
return _jsx(Box, { flexDirection: "column", children: lines.map((line, index) => {
|
|
65
|
+
const row = /^(\s*)([✓✗…])\s+(\S+)(.*)$/.exec(line);
|
|
66
|
+
if (!row) {
|
|
67
|
+
return _jsxs(Text, { color: theme.muted, children: [index === 0 && streaming ? _jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, line] }, `${index}-${line}`);
|
|
68
|
+
}
|
|
69
|
+
const [, indent, icon, toolName, rest] = row;
|
|
70
|
+
const iconColor = icon === '✓' ? theme.success : icon === '✗' ? theme.danger : theme.muted;
|
|
71
|
+
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
|
+
}) });
|
|
73
|
+
}
|
|
74
|
+
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)';
|
|
85
|
+
return [
|
|
86
|
+
'Provider configuration',
|
|
87
|
+
`- Provider: ${provider}`,
|
|
88
|
+
`- Model: ${model} (${modelSource})`,
|
|
89
|
+
`- Base URL: ${baseURL} (${baseURLSource})`,
|
|
90
|
+
`- API key: ${apiKeySource === 'missing' ? 'not configured; run /login or set OPENAI_API_KEY' : `configured via ${apiKeySource}`}`,
|
|
91
|
+
].join('\n');
|
|
92
|
+
}
|
|
93
|
+
function ChatScreen({ debug = false, version }) {
|
|
15
94
|
const { exit } = useApp();
|
|
16
95
|
const { stdout } = useStdout();
|
|
17
96
|
const height = stdout.rows ?? process.stdout.rows ?? 24;
|
|
18
97
|
const [messages, setMessages] = useState([
|
|
19
|
-
{ role: 'system', text: 'Welcome to Haze. Use /
|
|
98
|
+
{ role: 'system', text: 'Welcome to Haze. Use /help for commands.' }
|
|
20
99
|
]);
|
|
21
100
|
const [settings, setSettings] = useState({});
|
|
22
101
|
const conversationRef = useRef([]);
|
|
23
102
|
const lastAssistantTextRef = useRef('');
|
|
103
|
+
const abortControllerRef = useRef(null);
|
|
24
104
|
const [inputHistory, setInputHistory] = useState([]);
|
|
25
105
|
const [debugLogs, setDebugLogs] = useState([]);
|
|
26
106
|
const [contextFiles, setContextFiles] = useState([]);
|
|
27
107
|
const [mode, setMode] = useState('chat');
|
|
28
108
|
const [busy, setBusy] = useState(false);
|
|
109
|
+
const [busyLabel, setBusyLabel] = useState('Haze is thinking');
|
|
110
|
+
const [skills, setSkills] = useState([]);
|
|
111
|
+
const [branchName, setBranchName] = useState();
|
|
29
112
|
useEffect(() => {
|
|
30
|
-
readSettings()
|
|
113
|
+
Promise.all([readSettings(), currentBranchName()]).then(([next, branch]) => {
|
|
114
|
+
setSettings(next);
|
|
115
|
+
setBranchName(branch);
|
|
116
|
+
setMessages(m => [...m, { role: 'system', text: startupProviderInfo(next) }]);
|
|
117
|
+
}).catch(() => {
|
|
118
|
+
currentBranchName().then(branch => {
|
|
119
|
+
setBranchName(branch);
|
|
120
|
+
setMessages(m => [...m, { role: 'system', text: startupProviderInfo({}) }]);
|
|
121
|
+
}).catch(() => {
|
|
122
|
+
setMessages(m => [...m, { role: 'system', text: startupProviderInfo({}) }]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
31
125
|
readInputHistory().then(setInputHistory).catch(() => undefined);
|
|
32
126
|
readContextFiles().then(setContextFiles).catch(() => undefined);
|
|
127
|
+
refreshSkills().catch(() => undefined);
|
|
128
|
+
const branchTimer = setInterval(() => {
|
|
129
|
+
currentBranchName().then(setBranchName).catch(() => setBranchName(undefined));
|
|
130
|
+
}, 3000);
|
|
131
|
+
return () => clearInterval(branchTimer);
|
|
33
132
|
}, []);
|
|
34
133
|
function persistInputHistory(value) {
|
|
35
134
|
addInputHistoryItem(value).then(setInputHistory).catch(() => undefined);
|
|
36
135
|
}
|
|
136
|
+
async function refreshSkills() {
|
|
137
|
+
const registry = await loadSkillRegistry();
|
|
138
|
+
const nextSkills = [...registry.skills.values()];
|
|
139
|
+
setSkills(nextSkills);
|
|
140
|
+
return nextSkills;
|
|
141
|
+
}
|
|
142
|
+
function skillInvocation(value) {
|
|
143
|
+
if (!value.startsWith('/'))
|
|
144
|
+
return undefined;
|
|
145
|
+
const name = value.slice(1).trim();
|
|
146
|
+
if (!name || name.includes(' '))
|
|
147
|
+
return undefined;
|
|
148
|
+
return skills.find(skill => skill.name === name);
|
|
149
|
+
}
|
|
37
150
|
function debugLog(line) {
|
|
38
151
|
if (!debug)
|
|
39
152
|
return;
|
|
@@ -44,6 +157,12 @@ function ChatScreen({ debug = false }) {
|
|
|
44
157
|
lastAssistantTextRef.current = '';
|
|
45
158
|
setMessages([{ role: 'system', text: 'Cleared. The void is productive.' }]);
|
|
46
159
|
}
|
|
160
|
+
function cancelThinking() {
|
|
161
|
+
if (!busy)
|
|
162
|
+
return;
|
|
163
|
+
abortControllerRef.current?.abort('User pressed Esc.');
|
|
164
|
+
setBusy(false);
|
|
165
|
+
}
|
|
47
166
|
async function submit(value) {
|
|
48
167
|
if (busy)
|
|
49
168
|
return;
|
|
@@ -51,16 +170,22 @@ function ChatScreen({ debug = false }) {
|
|
|
51
170
|
const next = await updateSettings({ provider: 'openrouter', apiKey: value, baseURL: 'https://openrouter.ai/api/v1' });
|
|
52
171
|
setSettings(next);
|
|
53
172
|
setMode('chat');
|
|
54
|
-
setMessages(m => [...m, { role: 'system', text:
|
|
173
|
+
setMessages(m => [...m, { role: 'system', text: `OpenRouter login saved to ~/.haze/settings.json. Security theatre completed.\n\n${startupProviderInfo(next)}` }]);
|
|
55
174
|
return;
|
|
56
175
|
}
|
|
57
176
|
if (mode === 'model') {
|
|
58
177
|
const next = await updateSettings({ model: value });
|
|
59
178
|
setSettings(next);
|
|
60
179
|
setMode('chat');
|
|
61
|
-
setMessages(m => [...m, { role: 'system', text: `Model set to ${value}
|
|
180
|
+
setMessages(m => [...m, { role: 'system', text: `Model set to ${value}.\n\n${startupProviderInfo(next)}` }]);
|
|
62
181
|
return;
|
|
63
182
|
}
|
|
183
|
+
const invokedSkill = skillInvocation(value);
|
|
184
|
+
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);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const isSkillCreate = /^\/skills? create(?:\s|$)/.test(value);
|
|
64
189
|
const ctx = {
|
|
65
190
|
settings,
|
|
66
191
|
contextFiles,
|
|
@@ -69,13 +194,39 @@ function ChatScreen({ debug = false }) {
|
|
|
69
194
|
clearConversation,
|
|
70
195
|
runAgentTurn: (prompt, displayValue) => doAgentTurn(prompt, displayValue),
|
|
71
196
|
refreshContextFiles: async () => { const files = await readContextFiles().catch(() => contextFiles); setContextFiles(files); return files; },
|
|
72
|
-
updateSettings: patch => {
|
|
197
|
+
updateSettings: async (patch) => {
|
|
198
|
+
const next = await updateSettings(patch);
|
|
199
|
+
setSettings(next);
|
|
200
|
+
return next;
|
|
201
|
+
},
|
|
73
202
|
};
|
|
74
|
-
|
|
203
|
+
let result;
|
|
204
|
+
if (isSkillCreate) {
|
|
205
|
+
setBusyLabel('Creating skill');
|
|
206
|
+
setBusy(true);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
result = await handleSlashCommand(value, ctx);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
213
|
+
setMessages(m => [...m, { role: 'system', text: `Skill creation failed: ${text}` }]);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
if (isSkillCreate) {
|
|
218
|
+
setBusy(false);
|
|
219
|
+
setBusyLabel('Haze is thinking');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
75
222
|
if (result === 'exit')
|
|
76
223
|
return exit();
|
|
77
|
-
if (result === 'handled')
|
|
224
|
+
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 ')) {
|
|
226
|
+
await refreshSkills().catch(() => undefined);
|
|
227
|
+
}
|
|
78
228
|
return;
|
|
229
|
+
}
|
|
79
230
|
await doAgentTurn(value);
|
|
80
231
|
}
|
|
81
232
|
async function doAgentTurn(value, displayValue) {
|
|
@@ -89,11 +240,52 @@ function ChatScreen({ debug = false }) {
|
|
|
89
240
|
getConversation: () => conversationRef.current,
|
|
90
241
|
getLastAssistantText: () => lastAssistantTextRef.current,
|
|
91
242
|
setLastAssistantText: text => { lastAssistantTextRef.current = text; },
|
|
243
|
+
setAbortController: controller => { abortControllerRef.current = controller; },
|
|
92
244
|
});
|
|
93
245
|
}
|
|
94
|
-
const visible = messages;
|
|
95
|
-
const placeholder = mode === 'apiKey' ? 'OpenRouter API key
|
|
96
|
-
|
|
246
|
+
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);
|
|
251
|
+
const headerSubtitle = hasLogin && hasChosenModel
|
|
252
|
+
? [
|
|
253
|
+
'A minimal LLM harness for growing your own workflows while you work.',
|
|
254
|
+
'',
|
|
255
|
+
'Start with simple chat, then teach Haze your habits with skills:',
|
|
256
|
+
'/skill create review my branch against main — tiny spell, useful goblin.',
|
|
257
|
+
'',
|
|
258
|
+
'The most adaptive workflow is the one you shape as you go.',
|
|
259
|
+
'',
|
|
260
|
+
'Guardrails are light: Haze lets the LLM work from the terminal almost like you,',
|
|
261
|
+
'while trying to stay scoped to this project.',
|
|
262
|
+
].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.';
|
|
264
|
+
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' ? [
|
|
269
|
+
{ value: '/help', description: 'Show commands', kind: 'command' },
|
|
270
|
+
{ value: '/login', description: 'Save an OpenRouter API key', kind: 'command' },
|
|
271
|
+
{ value: '/model', description: 'Choose a model', kind: 'command' },
|
|
272
|
+
{ 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' },
|
|
278
|
+
{ value: '/init', description: 'Create or update AGENTS.md project instructions', kind: 'command' },
|
|
279
|
+
{ value: '/clear', description: 'Clear conversation history', kind: 'command' },
|
|
280
|
+
{ value: '/exit', description: 'Exit Haze', kind: 'command' },
|
|
281
|
+
{ value: '/quit', description: 'Exit Haze', kind: 'command' },
|
|
282
|
+
...skills.map(skill => ({ value: `/${skill.name}`, description: skill.description, kind: 'skill' })),
|
|
283
|
+
] : [];
|
|
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 }) })] })] });
|
|
97
289
|
}
|
|
98
290
|
export async function chatCommand(options = {}) {
|
|
99
291
|
const app = render(_jsx(ChatScreen, { debug: options.debug }));
|