glitool 1.0.1 → 2.0.1

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 (55) hide show
  1. package/README.md +115 -48
  2. package/dist/agent.js +233 -37
  3. package/dist/agents/coder.js +46 -34
  4. package/dist/agents/debugger.js +111 -0
  5. package/dist/agents/explainer.js +2 -5
  6. package/dist/agents/git-agent.js +90 -0
  7. package/dist/agents/graph.js +214 -23
  8. package/dist/agents/judge.js +61 -0
  9. package/dist/agents/planner.js +31 -12
  10. package/dist/agents/planningAgent.js +41 -0
  11. package/dist/agents/refactorer.js +97 -0
  12. package/dist/agents/reviewer-agent.js +87 -0
  13. package/dist/agents/reviewer.js +6 -9
  14. package/dist/agents/types.js +1 -0
  15. package/dist/agents/validator.js +93 -0
  16. package/dist/agents/workflow.js +45 -0
  17. package/dist/auth.js +87 -0
  18. package/dist/commands/version.js +1 -0
  19. package/dist/config.js +4 -1
  20. package/dist/confirmHandler.js +4 -2
  21. package/dist/index.js +12 -25
  22. package/dist/llm/classifier.js +61 -0
  23. package/dist/llm/factory.js +58 -0
  24. package/dist/llm/router.js +191 -22
  25. package/dist/logger.js +25 -0
  26. package/dist/processEvents.js +1 -0
  27. package/dist/tools/bashTool.js +90 -0
  28. package/dist/tools/editFileTool.js +14 -3
  29. package/dist/tools/index.js +3 -1
  30. package/dist/tools/listFilesTool.js +19 -21
  31. package/dist/tools/processRegistry.js +36 -0
  32. package/dist/tools/readBackgroundOutput.js +29 -0
  33. package/dist/tools/readFileTool.js +64 -9
  34. package/dist/tools/searchCodeTool.js +14 -4
  35. package/dist/tools/webFetchTool.js +45 -0
  36. package/dist/tools/writeFileTool.js +9 -5
  37. package/dist/trust/riskScorer.js +29 -2
  38. package/dist/ui/App.js +384 -47
  39. package/dist/ui/AuthFlow.js +76 -0
  40. package/dist/ui/ConfirmCard.js +53 -0
  41. package/dist/ui/EscalationCard.js +22 -0
  42. package/dist/ui/ExplainCard.js +5 -0
  43. package/dist/ui/Pipeline.js +37 -0
  44. package/dist/ui/ProcessTrace.js +79 -0
  45. package/dist/ui/RoleRow.js +16 -0
  46. package/dist/ui/RoleRow.test.js +8 -0
  47. package/dist/ui/SlashPalette.js +32 -0
  48. package/dist/ui/StatusBar.js +44 -0
  49. package/dist/ui/ToolLog.js +62 -0
  50. package/dist/ui/Welcome.js +11 -0
  51. package/dist/ui/renderMarkdown.js +41 -0
  52. package/dist/ui/symbols.js +19 -0
  53. package/dist/ui/tokens.js +13 -0
  54. package/dist/version.js +1 -0
  55. package/package.json +56 -54
@@ -0,0 +1,76 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { colors } from './tokens.js';
5
+ import { startDeviceFlow, pollDeviceFlow, saveAuth } from '../auth.js';
6
+ const SPINNER = ['◐', '◓', '◑', '◒'];
7
+ export const AuthFlow = ({ onDone, onCancel }) => {
8
+ const [flow, setFlow] = useState(null);
9
+ const [phase, setPhase] = useState('loading');
10
+ const [errorMsg, setErrorMsg] = useState('');
11
+ const [frame, setFrame] = useState(0);
12
+ const pollTimer = useRef(null);
13
+ const spinTimer = useRef(null);
14
+ useEffect(() => {
15
+ spinTimer.current = setInterval(() => setFrame(f => (f + 1) % SPINNER.length), 180);
16
+ startDeviceFlow()
17
+ .then(data => {
18
+ setFlow(data);
19
+ setPhase('waiting');
20
+ pollTimer.current = setInterval(async () => {
21
+ try {
22
+ const result = await pollDeviceFlow(data.device_code);
23
+ if (result.status === 'complete' && result.access_token) {
24
+ clearInterval(pollTimer.current);
25
+ clearInterval(spinTimer.current);
26
+ const auth = {
27
+ token: result.access_token,
28
+ plan: result.plan ?? 'free',
29
+ email: result.email ?? '',
30
+ requestsRemaining: result.requests_remaining ?? 50,
31
+ resetDate: result.reset_date ?? '',
32
+ savedAt: new Date().toISOString(),
33
+ };
34
+ saveAuth(auth);
35
+ setPhase('success');
36
+ setTimeout(() => onDone(auth), 1200);
37
+ }
38
+ else if (result.status === 'expired') {
39
+ clearInterval(pollTimer.current);
40
+ clearInterval(spinTimer.current);
41
+ setPhase('expired');
42
+ }
43
+ }
44
+ catch {
45
+ // network hiccup — keep polling
46
+ }
47
+ }, 5000);
48
+ })
49
+ .catch(err => {
50
+ clearInterval(spinTimer.current);
51
+ setPhase('error');
52
+ setErrorMsg(err?.message ?? 'Could not reach Glitool server');
53
+ });
54
+ return () => {
55
+ if (pollTimer.current)
56
+ clearInterval(pollTimer.current);
57
+ if (spinTimer.current)
58
+ clearInterval(spinTimer.current);
59
+ };
60
+ }, []);
61
+ if (phase === 'loading') {
62
+ return (_jsxs(Box, { borderStyle: "round", borderColor: colors.amber, paddingX: 1, children: [_jsxs(Text, { color: colors.amber, children: [SPINNER[frame], " "] }), _jsx(Text, { color: colors.muted, children: "Connecting to Glitool..." })] }));
63
+ }
64
+ if (phase === 'error') {
65
+ return (_jsxs(Box, { borderStyle: "round", borderColor: colors.rust, paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.rust, children: ["\u2717 Sign-in failed: ", errorMsg] }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Type /signup to try again, or Esc to cancel." })] }));
66
+ }
67
+ if (phase === 'expired') {
68
+ return (_jsxs(Box, { borderStyle: "round", borderColor: colors.rust, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.rust, children: "\u2717 Code expired." }), _jsx(Text, { color: colors.muted, dimColor: true, children: "Type /signup to get a new code." })] }));
69
+ }
70
+ if (phase === 'success') {
71
+ return (_jsx(Box, { borderStyle: "round", borderColor: colors.sage, paddingX: 1, children: _jsx(Text, { color: colors.sage, children: "\u2713 Signed in! Glitool is ready." }) }));
72
+ }
73
+ // waiting
74
+ const activateUrl = `${flow.verification_uri}?code=${flow.user_code}`;
75
+ return (_jsxs(Box, { borderStyle: "round", borderColor: colors.amber, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.amber, bold: true, children: "Sign in to Glitool (free \u00B7 50 req/month)" }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.muted, children: "1. Open this URL in your browser:" }), _jsxs(Text, { color: "cyan", children: [" ", activateUrl] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.muted, children: ["2. Your terminal code: ", _jsx(Text, { color: "white", bold: true, children: flow.user_code })] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.muted, children: [SPINNER[frame], " Waiting for sign-in... ", _jsx(Text, { dimColor: true, children: "(Esc to cancel)" })] })] }));
76
+ };
@@ -0,0 +1,53 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { colors } from "./tokens.js";
5
+ import { symbols } from "./symbols.js";
6
+ function buildDiff(req) {
7
+ const lines = [];
8
+ if (req.type === 'write') {
9
+ const all = (req.content ?? '').split('\n');
10
+ all.slice(0, 20).forEach(text => lines.push({ type: 'add', text }));
11
+ if (all.length > 20) {
12
+ lines.push({ type: 'context', text: `...${all.length - 20} more lines` });
13
+ }
14
+ }
15
+ else {
16
+ (req.oldString ?? '').split('\n').forEach(text => lines.push({ type: 'remove', text }));
17
+ (req.newString ?? '').split('\n').forEach(text => lines.push({ type: 'add', text }));
18
+ }
19
+ return lines;
20
+ }
21
+ const DiffLine = ({ line }) => {
22
+ if (line.type === 'add') {
23
+ return _jsxs(Text, { color: colors.sage, children: [" ", symbols.add, " ", line.text] });
24
+ }
25
+ if (line.type === 'remove') {
26
+ return _jsxs(Text, { color: colors.rust, children: [" ", symbols.remove, " ", line.text] });
27
+ }
28
+ return _jsxs(Text, { color: colors.muted, children: [" ", line.text] });
29
+ };
30
+ export const ConfirmCard = ({ request, onChoice }) => {
31
+ useInput((input, key) => {
32
+ const lower = input.toLowerCase();
33
+ if (lower === 'y' || key.return) {
34
+ onChoice('y');
35
+ return;
36
+ }
37
+ if (lower === 'n' || key.escape) {
38
+ onChoice('n');
39
+ return;
40
+ }
41
+ if (lower === 'd') {
42
+ onChoice('d');
43
+ return;
44
+ }
45
+ });
46
+ const risk = request.risk ?? 'low';
47
+ const riskColor = risk === 'high' ? colors.rust : colors.mustard;
48
+ const verb = request.type === 'write' ? 'write' : 'edit';
49
+ const diffLines = buildDiff(request);
50
+ const added = diffLines.filter(l => l.type === 'add').length;
51
+ const removed = diffLines.filter(l => l.type === 'remove').length;
52
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.mustard, bold: true, children: symbols.warning }), _jsxs(Text, { color: colors.ink, bold: true, children: ["glitool wants to ", verb, " ", request.filePath] }), _jsx(Text, { color: colors.muted, children: " " }), _jsxs(Text, { color: riskColor, bold: true, children: ["risk \u00B7 ", risk] })] }), _jsxs(Box, { borderStyle: "single", borderColor: colors.line, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: colors.muted, children: request.filePath }) }), _jsxs(Text, { color: colors.sage, children: ["+", added] }), _jsx(Text, { color: colors.muted, children: " " }), _jsxs(Text, { color: colors.rust, children: ["-", removed] })] }), diffLines.map((line, i) => (_jsx(DiffLine, { line: line }, i)))] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.muted, children: "Apply this change? " }), _jsx(Text, { color: colors.amber, bold: true, children: "[d]" }), _jsx(Text, { color: colors.muted, children: " view full diff " }), _jsx(Text, { color: colors.amber, bold: true, children: "[n]" }), _jsx(Text, { color: colors.muted, children: " reject " }), _jsx(Text, { color: colors.amber, bold: true, children: "[y]" }), _jsx(Text, { color: colors.muted, children: " approve" })] })] }));
53
+ };
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { colors } from './tokens.js';
5
+ export const EscalationCard = ({ payload, onChoice }) => {
6
+ useInput((input) => {
7
+ const k = input.toLowerCase();
8
+ if (k === 'a')
9
+ onChoice('approve');
10
+ else if (k === 'c')
11
+ onChoice('correct');
12
+ else if (k === 'x')
13
+ onChoice('abort');
14
+ });
15
+ const planPreview = payload.plan.length > 400
16
+ ? payload.plan.slice(0, 400) + '…'
17
+ : payload.plan;
18
+ const outputPreview = payload.finalOutput.length > 300
19
+ ? payload.finalOutput.slice(0, 300) + '…'
20
+ : payload.finalOutput;
21
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.amber, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: colors.amber, children: `⚠ Escalation — Judge gave up after ${payload.trajectory.length} attempts` }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Original request:" }), _jsx(Text, { wrap: "wrap", children: payload.userMessage })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Plan:" }), _jsx(Text, { color: "white", wrap: "wrap", children: planPreview })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Judge trail:" }), payload.trajectory.map((t) => (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: ` #${t.iteration} ` }), _jsx(Text, { color: t.verdict === 'ok' ? 'green' : 'red', children: t.verdict.toUpperCase() }), _jsx(Text, { color: "gray", children: ` · ${t.failure_point ?? 'none'} · ` }), _jsx(Text, { wrap: "wrap", children: t.reason })] }, t.iteration)))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Current output (preview):" }), _jsx(Text, { wrap: "wrap", children: outputPreview })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, color: "green", children: "[a]" }), _jsx(Text, { children: ' approve as-is ' }), _jsx(Text, { bold: true, color: "yellow", children: "[c]" }), _jsx(Text, { children: ' give correction ' }), _jsx(Text, { bold: true, color: "red", children: "[x]" }), _jsx(Text, { children: ' abort' })] })] }));
22
+ };
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { colors } from "./tokens.js";
5
+ export const ExplainCard = ({ children, footer }) => (_jsxs(Box, { borderStyle: "single", borderColor: colors.amber, borderTop: false, borderRight: false, borderBottom: false, paddingLeft: 2, flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.amber, bold: true, children: "\uD83D\uDCA1 EXPLAIN" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: typeof children === 'string' ? (_jsx(Text, { color: colors.ink2, wrap: "wrap", children: children })) : (children) }), footer && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: footer }) }))] }));
@@ -0,0 +1,37 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useState, useEffect } from "react";
3
+ import { colors } from "./tokens.js";
4
+ import { Box, Text } from "ink";
5
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ const AGENT_META = {
7
+ planner: { label: 'Planner', badge: 'P', color: colors.violet },
8
+ workflow: { label: 'Workflow', badge: 'W', color: colors.mustard },
9
+ coder: { label: 'Coder', badge: 'C', color: colors.amber },
10
+ validator: { label: 'Validator', badge: 'V', color: colors.sage },
11
+ judge: { label: 'Judge', badge: 'J', color: colors.rust },
12
+ };
13
+ function useSpinner() {
14
+ const [frame, setFrame] = useState(0);
15
+ useEffect(() => {
16
+ const id = setInterval(() => setFrame(f => (f + 1) % SPINNER_FRAMES.length), 80);
17
+ return () => clearInterval(id);
18
+ }, []);
19
+ return SPINNER_FRAMES[frame];
20
+ }
21
+ const ActiveCard = ({ name, badge, color, detail, spinner }) => (_jsxs(Box, { borderStyle: "single", borderColor: color, paddingX: 1, marginBottom: 1, children: [_jsxs(Text, { color: color, bold: true, children: [badge, " "] }), _jsxs(Text, { color: "white", bold: true, children: [name, " "] }), _jsxs(Text, { color: color, children: [spinner, " "] }), _jsx(Text, { color: colors.muted, children: detail ?? 'working...' })] }));
22
+ const DoneRow = ({ agents }) => (_jsx(Box, { marginBottom: 1, paddingLeft: 1, children: agents.map((a, i) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: a.color, bold: true, children: a.badge }), _jsx(Text, { color: colors.sage, children: " \u2713 " }), i < agents.length - 1 && _jsx(Text, { color: colors.muted, children: "\u00B7 " })] }, a.label))) }));
23
+ export const Pipeline = ({ planner, workflow, coder, validator, judge }) => {
24
+ const spinner = useSpinner();
25
+ const states = [
26
+ ['planner', planner],
27
+ ['workflow', workflow],
28
+ ['coder', coder],
29
+ ['validator', validator],
30
+ ['judge', judge],
31
+ ];
32
+ const doneAgents = states
33
+ .filter(([, s]) => s.status === 'done')
34
+ .map(([key]) => AGENT_META[key]);
35
+ const activeEntry = states.find(([, s]) => s.status === 'active');
36
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: [doneAgents.length > 0 && _jsx(DoneRow, { agents: doneAgents }), activeEntry && (_jsx(ActiveCard, { name: AGENT_META[activeEntry[0]].label, badge: AGENT_META[activeEntry[0]].badge, color: AGENT_META[activeEntry[0]].color, detail: activeEntry[1].detail, spinner: spinner }))] }));
37
+ };
@@ -0,0 +1,79 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useState, useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ const STAGE_COLOR = {
5
+ planner: '#c4732e',
6
+ coder: '#3e8a9a',
7
+ validator: '#6c5ab8',
8
+ judge: '#5a8c5a',
9
+ debugger: '#b54226',
10
+ reviewer: '#6c5ab8',
11
+ refactorer: '#3e8a9a',
12
+ git_agent: '#5a8c5a',
13
+ };
14
+ const STAGE_LABEL = {
15
+ planner: 'PLANNER',
16
+ coder: 'CODER',
17
+ validator: 'VALIDATOR',
18
+ judge: 'JUDGE',
19
+ debugger: 'DEBUGGER',
20
+ reviewer: 'REVIEWER',
21
+ refactorer: 'REFACTORER',
22
+ git_agent: 'GIT',
23
+ };
24
+ const TOOL_VERB = {
25
+ readFile: 'read',
26
+ writeFile: 'write',
27
+ editFile: 'edit',
28
+ searchCode: 'search',
29
+ listFiles: 'list',
30
+ bash: 'run',
31
+ webFetch: 'fetch',
32
+ };
33
+ export const ProcessTrace = ({ events, active }) => {
34
+ const SPINNER = ['◐', '◓', '◑', '◒'];
35
+ const [frame, setFrame] = useState(0);
36
+ useEffect(() => {
37
+ if (!active)
38
+ return;
39
+ const id = setInterval(() => setFrame(f => (f + 1) % SPINNER.length), 150);
40
+ return () => clearInterval(id);
41
+ }, [active]);
42
+ if (events.length === 0)
43
+ return null;
44
+ const sections = [];
45
+ for (const ev of events) {
46
+ if (ev.type === 'stage_start') {
47
+ sections.push({ stage: ev.stage, items: [], done: false });
48
+ }
49
+ else if (ev.type === 'stage_done') {
50
+ if (sections.length > 0)
51
+ sections[sections.length - 1].done = true;
52
+ }
53
+ else if (ev.type === 'reasoning' || ev.type === 'tool') {
54
+ if (sections.length > 0) {
55
+ sections[sections.length - 1].items.push(ev);
56
+ }
57
+ }
58
+ }
59
+ return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: sections.map((sec, si) => {
60
+ const color = STAGE_COLOR[sec.stage];
61
+ const isLast = si === sections.length - 1;
62
+ const spinning = isLast && !sec.done && active;
63
+ const bullet = sec.done ? '●' : (spinning ? SPINNER[frame] : '○');
64
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: isLast ? 0 : 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: color, bold: true, children: [bullet, " ", STAGE_LABEL[sec.stage]] }), spinning && _jsx(Text, { color: color, children: " \u00B7\u00B7\u00B7" })] }), sec.items.map((item, ii) => {
65
+ if (item.type === 'reasoning') {
66
+ return item.text
67
+ .split('\n')
68
+ .filter(Boolean)
69
+ .slice(0, 4)
70
+ .map((line, li) => (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "#8a8170", italic: true, children: line.trim() }) }, `${ii}-${li}`)));
71
+ }
72
+ if (item.type === 'tool') {
73
+ const verb = TOOL_VERB[item.tool] ?? item.tool;
74
+ return (_jsxs(Box, { marginLeft: 2, children: [_jsxs(Text, { color: color, children: [" \u2713 ", verb] }), item.target ? (_jsxs(Text, { color: "#b3a994", children: [" ", item.target] })) : null] }, ii));
75
+ }
76
+ return null;
77
+ })] }, si));
78
+ }) }));
79
+ };
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from 'ink';
4
+ import { colors } from "./tokens.js";
5
+ const ROLE_COLOR = {
6
+ you: colors.amber,
7
+ glitool: colors.ink2,
8
+ tools: colors.violet,
9
+ graph: colors.violet,
10
+ plan: colors.sage,
11
+ note: colors.muted
12
+ };
13
+ export const RoleRow = ({ role, timestamp, children }) => {
14
+ const lable = timestamp ? `${role} . ${timestamp}` : role;
15
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: ROLE_COLOR[role], bold: true, children: lable.toUpperCase() }), _jsx(Box, { paddingLeft: 2, children: children })] }));
16
+ };
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { render, Box, Text } from 'ink';
4
+ import { RoleRow } from './RoleRow.js';
5
+ import { colors } from './tokens.js';
6
+ import { symbols } from './symbols.js';
7
+ const Demo = () => (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(RoleRow, { role: "you", timestamp: "14:08", children: _jsx(Text, { children: "where do we resolve the OpenAI key?" }) }), _jsx(RoleRow, { role: "glitool", children: _jsx(Text, { children: "The key is loaded from ~/.glitool/.env at startup." }) }), _jsx(RoleRow, { role: "tools", children: _jsxs(Text, { color: colors.sage, children: [symbols.done, " readFile \u2192 src/agent.ts"] }) }), _jsxs(RoleRow, { role: "plan", children: [_jsx(Text, { children: "1. Add /memory case" }), _jsx(Text, { children: "2. Spawn $EDITOR" }), _jsx(Text, { children: "3. Confirm before write" })] }), _jsx(RoleRow, { role: "note", children: _jsx(Text, { color: colors.muted, children: "tip: pass -e to enable explain mode" }) })] }));
8
+ render(_jsx(Demo, {}));
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from 'ink';
4
+ import { colors } from "./tokens.js";
5
+ export const SLASH_COMMANDS = [
6
+ { cmd: '/help', desc: 'List commands and shortcuts' },
7
+ { cmd: '/plan', desc: 'Force planning mode (complex tier)' },
8
+ { cmd: '/coder', desc: 'Force coding mode (standard tier, graph)' },
9
+ { cmd: '/quick', desc: 'Force quick chat (cheapest model)' },
10
+ { cmd: '/explain', desc: 'Explanation only — no file edits' },
11
+ { cmd: '/model', desc: 'Show or switch the active model' },
12
+ { cmd: '/memory', desc: 'View or edit project memory and session summary' },
13
+ { cmd: '/tools', desc: 'List enabled tools and their permissions' },
14
+ { cmd: '/clear', desc: 'Clear current chat (keeps memory)' },
15
+ { cmd: '/reset', desc: 'Clear chat and wipe memory + summary' },
16
+ { cmd: '/exit', desc: 'Save summary and quit' },
17
+ ];
18
+ export const SlashPalette = ({ items, selectedIndex }) => {
19
+ if (items.length === 0)
20
+ return null;
21
+ return (_jsxs(Box, { borderStyle: "single", borderColor: colors.line, flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { color: colors.muted, children: "commands \u00B7 type to filter \u00B7 \u2191\u2193 navigate \u00B7 \u23CE run \u00B7 esc dismiss" }), items.map((item, i) => {
22
+ const isSelected = i === selectedIndex;
23
+ return (_jsxs(Box, { children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: colors.amber, children: isSelected ? '›' : ' ' }) }), _jsx(Box, { width: 11, children: _jsx(Text, { color: isSelected ? colors.amber : colors.ink, bold: isSelected, children: item.cmd }) }), _jsx(Text, { color: isSelected ? colors.ink2 : colors.muted, children: item.desc })] }, item.cmd));
24
+ })] }));
25
+ };
26
+ export function filterCommands(input) {
27
+ if (!input.startsWith('/'))
28
+ return [];
29
+ if (input === '/')
30
+ return SLASH_COMMANDS;
31
+ return SLASH_COMMANDS.filter(c => c.cmd.startsWith(input));
32
+ }
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { colors } from "./tokens.js";
5
+ import { symbols } from "./symbols.js";
6
+ const STATE_COLOR = {
7
+ idle: colors.muted,
8
+ ready: colors.sage,
9
+ working: colors.amber,
10
+ awaiting: colors.mustard,
11
+ error: colors.rust,
12
+ };
13
+ const STATE_LABEL = {
14
+ idle: 'idle',
15
+ ready: 'ready',
16
+ working: 'working',
17
+ awaiting: 'awaiting confirm',
18
+ error: 'error',
19
+ };
20
+ function formatTokens(n) {
21
+ if (n < 1000)
22
+ return `${n} tokens`;
23
+ return `${(n / 1000).toFixed(1)}k tokens`;
24
+ }
25
+ function formatCost(c) {
26
+ return `$${c.toFixed(3)}`;
27
+ }
28
+ export const StatusBar = ({ state, detail, tier, anonLeft, model, tokens, cost }) => {
29
+ const dotColor = STATE_COLOR[state];
30
+ const stateLabel = STATE_LABEL[state];
31
+ const leftParts = [stateLabel];
32
+ if (detail)
33
+ leftParts.push(detail);
34
+ const rightParts = [];
35
+ if (tier) {
36
+ rightParts.push(`${tier}`);
37
+ }
38
+ else if (anonLeft !== undefined) {
39
+ rightParts.push(anonLeft > 0 ? `${anonLeft} free left` : 'sign up for more');
40
+ }
41
+ rightParts.push(model);
42
+ rightParts.push(formatTokens(tokens));
43
+ return (_jsxs(Box, { borderStyle: "single", borderColor: colors.line, paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, children: symbols.statusDot }), _jsx(Text, { color: colors.ink2, children: leftParts.join(' . ') })] }), _jsx(Box, { children: _jsx(Text, { color: colors.muted, children: rightParts.join(' · ') }) })] }));
44
+ };
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import React, { useEffect, useState } from "react";
4
+ import { spinnerFrames } from "./symbols.js";
5
+ import { colors } from "./tokens.js";
6
+ const TOOL_ICON = {
7
+ bash: '$',
8
+ writeFile: '✎',
9
+ editFile: '✎',
10
+ readFile: '≡',
11
+ listFiles: '≡',
12
+ searchCode: '⌕',
13
+ webFetch: '↗',
14
+ readBackgroundOutput: '⊙',
15
+ };
16
+ const TOOL_COLOR = {
17
+ bash: colors.mustard,
18
+ writeFile: colors.amber,
19
+ editFile: colors.amber,
20
+ readFile: colors.teal,
21
+ listFiles: colors.teal,
22
+ searchCode: colors.teal,
23
+ webFetch: colors.violet,
24
+ };
25
+ function formatTarget(tool, target) {
26
+ let value = target;
27
+ try {
28
+ const parsed = JSON.parse(target);
29
+ value = parsed.command ?? parsed.filePath ?? parsed.pattern
30
+ ?? parsed.query ?? parsed.url ?? target;
31
+ }
32
+ catch { }
33
+ if (tool === 'bash') {
34
+ value = value.replace(/^npx\s+/, '').replace(/^node\s+/, '');
35
+ }
36
+ return value.length > 55 ? value.slice(0, 52) + '…' : value;
37
+ }
38
+ export const ToolLog = ({ entries }) => {
39
+ const [tick, setTick] = useState(0);
40
+ useEffect(() => {
41
+ if (!entries.some(e => e.status === 'running'))
42
+ return;
43
+ const id = setInterval(() => setTick(t => t + 1), 120);
44
+ return () => clearInterval(id);
45
+ }, [entries]);
46
+ if (entries.length === 0)
47
+ return null;
48
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: entries.map((e, i) => (_jsx(ToolLogRow, { entry: e, tick: tick }, i))) }));
49
+ };
50
+ const ToolLogRow = ({ entry, tick }) => {
51
+ const isRunning = entry.status === 'running';
52
+ const isDone = entry.status === 'done';
53
+ const spinner = spinnerFrames[tick % spinnerFrames.length];
54
+ const icon = isRunning ? spinner : isDone ? '✓' : '·';
55
+ const iconColor = isRunning ? colors.amber : isDone ? colors.sage : colors.muted;
56
+ const toolColor = TOOL_COLOR[entry.tool] ?? colors.muted2;
57
+ const toolIcon = TOOL_ICON[entry.tool] ?? '·';
58
+ const target = formatTarget(entry.tool, entry.target);
59
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: iconColor, children: [icon, " "] }), _jsxs(Text, { color: toolColor, children: [toolIcon, " "] }), _jsx(Text, { color: toolColor, dimColor: isDone, children: entry.tool }), _jsx(Text, { color: colors.muted, children: " \u00B7 " }), _jsx(Text, { color: isDone ? colors.muted : colors.muted2, children: target }), entry.stat
60
+ ? _jsxs(Text, { color: colors.muted, children: [" ", entry.stat] })
61
+ : null] }));
62
+ };
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from 'ink';
4
+ import { colors } from "./tokens.js";
5
+ const InfoRow = ({ label, children }) => (_jsxs(Box, { children: [_jsx(Box, { children: _jsx(Text, { color: colors.muted }) }), _jsx(Box, { children: children })] }));
6
+ const Card = ({ title, badge, children }) => (_jsxs(Box, { borderStyle: "single", borderColor: colors.line, flexDirection: "column", paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, bold: true, children: title }), badge && _jsxs(Text, { color: colors.sage, children: [" [", badge, "]"] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children })] }));
7
+ export const Welcome = ({ name, version, workspace, runtime, sessionResumed = false, priorTurns = 0, }) => {
8
+ const subtitle = sessionResumed ? `AI coding assistant · session resumed · ${priorTurns} prior turns summarized` : 'AI coding assistant';
9
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Box, { width: 4, children: _jsx(Text, { color: colors.amber, bold: true, children: "g" }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Text, { color: colors.ink, bold: true, children: ["Welcome back, ", name, "."] }), _jsx(Text, { color: colors.muted, children: subtitle })] }), _jsxs(Text, { color: colors.muted, children: ["v", version] })] }), _jsxs(Card, { title: "Workspace", badge: "indexed", children: [_jsx(InfoRow, { label: "path", children: _jsx(Text, { color: colors.ink, children: workspace.path }) }), _jsx(InfoRow, { label: "branch", children: _jsx(Text, { color: colors.ink, children: workspace.branch }) }), _jsx(InfoRow, { label: "files", children: _jsxs(Text, { color: colors.ink, children: [workspace.files, workspace.loc ? ` · ${workspace.loc}` : ''] }) })] }), _jsxs(Card, { title: "Runtime", children: [_jsxs(InfoRow, { label: "model", children: [_jsx(Text, { color: colors.ink, children: runtime.model }), runtime.routerOn && _jsx(Text, { color: colors.amber, children: " [router on]" })] }), _jsx(InfoRow, { label: "tools", children: _jsxs(Text, { color: colors.ink, children: [runtime.toolsCount, " enabled"] }) }), _jsxs(InfoRow, { label: "explain", children: [_jsx(Text, { color: colors.ink, children: runtime.explainOn ? 'on' : 'off' }), !runtime.explainOn && _jsx(Text, { color: colors.muted, children: " \u00B7 pass -e to enable" })] })] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(QuickHelp, { cmd: "/help", desc: "List commands and shortcuts" }), _jsx(QuickHelp, { cmd: "/model", desc: "Show or switch the active model" }), _jsx(QuickHelp, { cmd: "/tools", desc: "List available tools" }), _jsx(QuickHelp, { cmd: "/clear", desc: "Clear current session" }), _jsx(QuickHelp, { cmd: "/exit", desc: "Save summary and quit" })] })] }));
10
+ };
11
+ const QuickHelp = ({ cmd, desc }) => (_jsxs(Box, { children: [_jsx(Box, { width: 10, children: _jsx(Text, { color: colors.amber, bold: true, children: cmd }) }), _jsx(Text, { color: colors.muted, children: desc })] }));
@@ -0,0 +1,41 @@
1
+ import chalk, {} from 'chalk';
2
+ const SEVERITY = {
3
+ CRITICAL: chalk.hex('#b54226').bold, // rust — danger
4
+ WARNING: chalk.hex('#c69a3a').bold, // mustard — caution
5
+ SUGGESTION: chalk.hex('#6c5ab8').bold, // violet — info
6
+ GOOD: chalk.hex('#5a8c5a').bold, // sage — success
7
+ SUMMARY: chalk.hex('#c4732e').bold, // amber — neutral header
8
+ ERROR: chalk.hex('#b54226').bold, // rust
9
+ NOTE: chalk.hex('#c69a3a').bold, // mustard
10
+ INFO: chalk.hex('#3e8a9a').bold, // teal
11
+ };
12
+ export function renderMarkdown(text) {
13
+ return text
14
+ // fenced code blocks ```...```
15
+ .replace(/```(?:\w+)?\n([\s\S]+?)```/gm, (_, code) => chalk.hex('#3e8a9a')(code.trimEnd()))
16
+ // ## H1/H2 → amber bold uppercase + padding
17
+ .replace(/^#{1,2} (.+)$/gm, (_, t) => '\n' + chalk.hex('#c4732e').bold(t.trim().toUpperCase()) + '\n')
18
+ // ### H3 → amber normal
19
+ .replace(/^### (.+)$/gm, (_, t) => chalk.hex('#c4732e')(t.trim()))
20
+ // **bold** → bold white
21
+ .replace(/\*\*(.+?)\*\*/g, (_, t) => chalk.bold.white(t))
22
+ // *italic* → muted italic
23
+ .replace(/\*([^*\n]+)\*/g, (_, t) => chalk.italic.hex('#8a8170')(t))
24
+ // `inline code` → teal
25
+ .replace(/`([^`\n]+)`/g, (_, t) => chalk.hex('#3e8a9a')(t))
26
+ // severity keywords on their own line (CRITICAL, WARNING etc)
27
+ .replace(/^(CRITICAL|WARNING|SUGGESTION|GOOD|SUMMARY|ERROR|NOTE|INFO)$/gm, (_, kw) => (SEVERITY[kw] ?? chalk.bold)(kw))
28
+ // severity keywords inline
29
+ .replace(/\b(CRITICAL|WARNING|SUGGESTION|GOOD|ERROR|NOTE|INFO)\b/g, (_, kw) => (SEVERITY[kw] ?? chalk.bold)(kw))
30
+ // file:line references → muted2 (readable but not distracting)
31
+ .replace(/\b([\w./\\-]+\.(ts|tsx|js|jsx|py|go|rs|json|md)):(\d+)(-\d+)?\b/g, (m) => chalk.hex('#b3a994')(m))
32
+ // --- horizontal rule → dim line
33
+ .replace(/^---+$/gm, chalk.hex('#8a8170')('─'.repeat(50)))
34
+ // numbered list → mustard number
35
+ .replace(/^(\d+)\. (.+)$/gm, (_, n, t) => ` ${chalk.hex('#c69a3a').bold(n + '.')} ${t}`)
36
+ // - bullet → amber arrow
37
+ .replace(/^[-*] (.+)$/gm, (_, t) => ` ${chalk.hex('#c4732e')('→')} ${t}`)
38
+ // _none_ → dimmed
39
+ .replace(/^_([^_\n]+)_$/gm, (_, t) => chalk.dim(t))
40
+ .trimEnd();
41
+ }
@@ -0,0 +1,19 @@
1
+ export const symbols = {
2
+ // input prefixes
3
+ prompt: '›',
4
+ confirmPrompt: '?',
5
+ // status
6
+ statusDot: '■',
7
+ // tool/agent state
8
+ done: '✓',
9
+ running: '●',
10
+ queued: '◌',
11
+ // diff
12
+ add: '+',
13
+ remove: '-',
14
+ // misc
15
+ arrow: '→',
16
+ warning: '!',
17
+ };
18
+ // Spinner frames for "running" state — cycle with setInterval(200ms)
19
+ export const spinnerFrames = ['◌', '◐', '●', '◑'];
@@ -0,0 +1,13 @@
1
+ export const colors = {
2
+ ink: '#1f1b16',
3
+ ink2: '#3b3429',
4
+ muted: '#8a8170',
5
+ muted2: '#b3a994',
6
+ line: '#e6ddc9',
7
+ amber: '#c4732e',
8
+ sage: '#5a8c5a',
9
+ rust: '#b54226',
10
+ mustard: '#c69a3a',
11
+ violet: '#6c5ab8',
12
+ teal: '#3e8a9a',
13
+ };
@@ -0,0 +1 @@
1
+ export {};