@bahulam/code 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent block renderer — Mission Control (PRD-055 §7).
|
|
3
|
+
*
|
|
4
|
+
* Renders the open/close pair for a sub-agent block, dimmed throughout so
|
|
5
|
+
* the primary agent reads bright by contrast. Inner tool cards are indented
|
|
6
|
+
* via the `subAgentIndent()` helper so they nest visually under the header.
|
|
7
|
+
*
|
|
8
|
+
* 🛰️ explore "JWT lifecycle" ▸ running
|
|
9
|
+
* 🔭 Search code "expire" → 6 matches
|
|
10
|
+
* 🔭 Read file auth.py L120-180 → 60 lines
|
|
11
|
+
* └ ✅ returned 3 files identified · $0.004 · 2.1s
|
|
12
|
+
*
|
|
13
|
+
* Maintains a depth stack so concurrent / nested sub-agents indent further
|
|
14
|
+
* and so callers can ask `inSubAgent()` / `depth()` without threading state.
|
|
15
|
+
*
|
|
16
|
+
* No I/O — caller writes the returned strings to stderr. This keeps the
|
|
17
|
+
* module testable from a plain Node script.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { paint } from './palette.mjs';
|
|
21
|
+
import { icons } from './icons.mjs';
|
|
22
|
+
|
|
23
|
+
const SUB_ICONS = {
|
|
24
|
+
explore: '🔭',
|
|
25
|
+
plan: '📐',
|
|
26
|
+
verify: '✅',
|
|
27
|
+
debug: '🪲',
|
|
28
|
+
refactor:'♻️',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── Active stack ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const _stack = []; // [{ id, type, startedAt }]
|
|
34
|
+
|
|
35
|
+
/** How many sub-agents are currently open. */
|
|
36
|
+
export function depth() { return _stack.length; }
|
|
37
|
+
export function inSubAgent() { return _stack.length > 0; }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Indent string for a tool card line nested under N sub-agents.
|
|
41
|
+
* 5 cols per level matches the existing `' '` legacy indent.
|
|
42
|
+
*/
|
|
43
|
+
export function subAgentIndent(extraDepth = 0) {
|
|
44
|
+
const d = _stack.length + extraDepth;
|
|
45
|
+
if (d <= 0) return ' ';
|
|
46
|
+
return ' '.repeat(2 + d * 3);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Render ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open a sub-agent block. Pushes onto the stack; returns the lines to print.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string} ANSI-styled multi-line block (no trailing newline).
|
|
55
|
+
*/
|
|
56
|
+
export function renderSubAgentOpen({ id, type, query, parentDepth } = {}) {
|
|
57
|
+
const t = type || 'sub-agent';
|
|
58
|
+
const depthBefore = _stack.length;
|
|
59
|
+
_stack.push({ id: id || `${t}-${depthBefore}-${tag()}`, type: t, startedAt: Date.now() });
|
|
60
|
+
|
|
61
|
+
const indent = ' '.repeat(2 + depthBefore * 3);
|
|
62
|
+
const iconChar = SUB_ICONS[t] || icons.subAgent;
|
|
63
|
+
const head = `${indent}${iconChar} ${paint.brand.data(t)} ${paint.text.dim(`"${query || ''}"`)}`;
|
|
64
|
+
const tag1 = paint.text.dim('▸ running');
|
|
65
|
+
|
|
66
|
+
return query
|
|
67
|
+
? `\n${head} ${tag1}`
|
|
68
|
+
: `\n${indent}${iconChar} ${paint.brand.data(t)} ${tag1}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Close the most recent sub-agent block. Pops the stack; returns the close
|
|
73
|
+
* line with optional cost / token / duration attribution per PRD §7.3.
|
|
74
|
+
*
|
|
75
|
+
* └ ✅ returned 3 files identified · 1.2k tok · $0.004 · 2.1s
|
|
76
|
+
* └ ✗ explore agent failed
|
|
77
|
+
*
|
|
78
|
+
* Caller passes `success` (default true), `summary` ("returned N files"),
|
|
79
|
+
* and any of `{ costUsd, tokens, durationS, toolCalls, iterations }`.
|
|
80
|
+
*/
|
|
81
|
+
export function renderSubAgentClose({
|
|
82
|
+
type,
|
|
83
|
+
success = true,
|
|
84
|
+
summary = '',
|
|
85
|
+
costUsd,
|
|
86
|
+
tokens,
|
|
87
|
+
durationS,
|
|
88
|
+
toolCalls,
|
|
89
|
+
iterations,
|
|
90
|
+
error,
|
|
91
|
+
} = {}) {
|
|
92
|
+
// Match-pop: if the type doesn't match the top of stack we still pop the
|
|
93
|
+
// top entry — backends never emit interleaved open/close, so this is the
|
|
94
|
+
// safe behavior.
|
|
95
|
+
const opened = _stack.pop();
|
|
96
|
+
const t = type || opened?.type || 'sub-agent';
|
|
97
|
+
const indent = ' '.repeat(2 + _stack.length * 3);
|
|
98
|
+
|
|
99
|
+
if (!success) {
|
|
100
|
+
const line = `${indent}${paint.text.dim('└')} ${paint.state.danger('✗')} ${paint.text.dim(`${t} agent failed`)}`;
|
|
101
|
+
if (error) {
|
|
102
|
+
return `${line}\n${indent} ${paint.state.danger(truncate(error, 140))}`;
|
|
103
|
+
}
|
|
104
|
+
return line;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (toolCalls > 0) parts.push(`${toolCalls} tools`);
|
|
109
|
+
if (iterations > 0) parts.push(`${iterations} iter`);
|
|
110
|
+
if (tokens > 0) parts.push(`${formatTokens(tokens)} tok`);
|
|
111
|
+
if (typeof costUsd === 'number' && costUsd > 0) parts.push(formatCost(costUsd));
|
|
112
|
+
if (durationS != null) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
113
|
+
const detail = parts.length ? paint.text.dim(' · ' + parts.join(' · ')) : '';
|
|
114
|
+
|
|
115
|
+
const body = summary
|
|
116
|
+
? paint.text.dim(summary)
|
|
117
|
+
: paint.text.dim(`${t} returned`);
|
|
118
|
+
|
|
119
|
+
return `${indent}${paint.text.dim('└')} ${paint.state.success('✅')} ${body}${detail}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Force-clear the stack. Use after a `complete` event or when cancelling so
|
|
124
|
+
* a stale entry doesn't keep indenting future output.
|
|
125
|
+
*/
|
|
126
|
+
export function resetSubAgents() { _stack.length = 0; }
|
|
127
|
+
|
|
128
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
function truncate(text, n) {
|
|
131
|
+
const s = String(text || '');
|
|
132
|
+
return s.length <= n ? s : s.slice(0, n - 1) + '…';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatTokens(n) {
|
|
136
|
+
if (!Number.isFinite(n)) return '0';
|
|
137
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
138
|
+
return String(Math.round(n));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatCost(usd) {
|
|
142
|
+
if (usd < 0.001) return `$${usd.toFixed(5)}`;
|
|
143
|
+
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
|
144
|
+
return `$${usd.toFixed(3)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function tag() {
|
|
148
|
+
// Avoid Date.now()/Math.random() drift across re-renders — depth+counter is
|
|
149
|
+
// enough to keep ids unique within a process.
|
|
150
|
+
tag._n = (tag._n || 0) + 1;
|
|
151
|
+
return tag._n.toString(36);
|
|
152
|
+
}
|
package/src/ui/term.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection.
|
|
3
|
+
*
|
|
4
|
+
* Resolves once at import. Re-evaluation requires `refresh()` (used by tests
|
|
5
|
+
* and the rare command that toggles a relevant env var mid-process).
|
|
6
|
+
*
|
|
7
|
+
* Capability tiers (highest first):
|
|
8
|
+
* truecolor - 24-bit RGB (e.g. iTerm2, modern xterm, Windows Terminal)
|
|
9
|
+
* ansi256 - 256-color palette
|
|
10
|
+
* ansi16 - basic 16 colors
|
|
11
|
+
* none - no color (NO_COLOR=1, dumb terminal, non-TTY without override)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const TRUECOLOR_TERMS = new Set([
|
|
15
|
+
'truecolor',
|
|
16
|
+
'24bit',
|
|
17
|
+
'24-bit',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const ANSI256_TERMS = [
|
|
21
|
+
/-256(color)?$/i,
|
|
22
|
+
/^xterm/i,
|
|
23
|
+
/^screen/i,
|
|
24
|
+
/^tmux/i,
|
|
25
|
+
/^rxvt-unicode/i,
|
|
26
|
+
/^alacritty/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const DUMB_TERMS = new Set(['', 'dumb', 'unknown']);
|
|
30
|
+
|
|
31
|
+
function readEnv() {
|
|
32
|
+
const env = process.env || {};
|
|
33
|
+
return {
|
|
34
|
+
NO_COLOR: env.NO_COLOR,
|
|
35
|
+
FORCE_COLOR: env.FORCE_COLOR,
|
|
36
|
+
KEPLER_PLAIN: env.KEPLER_PLAIN,
|
|
37
|
+
COLORTERM: (env.COLORTERM || '').toLowerCase(),
|
|
38
|
+
TERM: (env.TERM || '').toLowerCase(),
|
|
39
|
+
TERM_PROGRAM: env.TERM_PROGRAM || '',
|
|
40
|
+
CI: env.CI,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectColorLevel(env, isTTY) {
|
|
45
|
+
// Hard opt-out (https://no-color.org). Honored even on TTYs.
|
|
46
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
|
|
47
|
+
if (env.KEPLER_PLAIN === '1') return 'none';
|
|
48
|
+
|
|
49
|
+
// Hard opt-in. FORCE_COLOR=1|2|3 maps to ansi16|ansi256|truecolor.
|
|
50
|
+
// FORCE_COLOR with no value or =true falls through to detection.
|
|
51
|
+
if (env.FORCE_COLOR !== undefined) {
|
|
52
|
+
const v = String(env.FORCE_COLOR).trim();
|
|
53
|
+
if (v === '0' || v === 'false') return 'none';
|
|
54
|
+
if (v === '1') return 'ansi16';
|
|
55
|
+
if (v === '2') return 'ansi256';
|
|
56
|
+
if (v === '3') return 'truecolor';
|
|
57
|
+
// Any other truthy value: continue detection but allow non-TTY.
|
|
58
|
+
isTTY = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// No TTY and not forced: no color.
|
|
62
|
+
if (!isTTY) return 'none';
|
|
63
|
+
|
|
64
|
+
if (DUMB_TERMS.has(env.TERM)) return 'none';
|
|
65
|
+
|
|
66
|
+
if (TRUECOLOR_TERMS.has(env.COLORTERM)) return 'truecolor';
|
|
67
|
+
|
|
68
|
+
// Some terminal emulators advertise truecolor through TERM_PROGRAM.
|
|
69
|
+
if (env.TERM_PROGRAM === 'iTerm.app' || env.TERM_PROGRAM === 'WezTerm') {
|
|
70
|
+
return 'truecolor';
|
|
71
|
+
}
|
|
72
|
+
if (env.TERM_PROGRAM === 'vscode' || env.TERM_PROGRAM === 'Apple_Terminal') {
|
|
73
|
+
return 'ansi256';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (ANSI256_TERMS.some(re => re.test(env.TERM))) return 'ansi256';
|
|
77
|
+
|
|
78
|
+
return 'ansi16';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function detectUnicode(env) {
|
|
82
|
+
if (env.KEPLER_PLAIN === '1') return false;
|
|
83
|
+
// Most modern terminals on macOS/Linux handle UTF-8.
|
|
84
|
+
// Windows ConEmu / older terminals are the main holdouts; conservative
|
|
85
|
+
// fallback when LANG and LC_* are missing or explicitly POSIX.
|
|
86
|
+
const lang = (process.env.LANG || process.env.LC_ALL || process.env.LC_CTYPE || '').toLowerCase();
|
|
87
|
+
if (!lang) return process.platform !== 'win32';
|
|
88
|
+
if (lang.includes('utf')) return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compute() {
|
|
93
|
+
const env = readEnv();
|
|
94
|
+
const isTTY = !!(process.stdout && process.stdout.isTTY);
|
|
95
|
+
const level = detectColorLevel(env, isTTY);
|
|
96
|
+
return {
|
|
97
|
+
isTTY,
|
|
98
|
+
colorLevel: level, // 'none' | 'ansi16' | 'ansi256' | 'truecolor'
|
|
99
|
+
color: level !== 'none',
|
|
100
|
+
truecolor: level === 'truecolor',
|
|
101
|
+
ansi256: level === 'ansi256' || level === 'truecolor',
|
|
102
|
+
unicode: detectUnicode(env),
|
|
103
|
+
plain: env.KEPLER_PLAIN === '1',
|
|
104
|
+
columns: (process.stdout && process.stdout.columns) || 80,
|
|
105
|
+
rows: (process.stdout && process.stdout.rows) || 24,
|
|
106
|
+
ci: !!env.CI,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let _capabilities = compute();
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Current terminal capabilities. Stable until `refresh()` is called.
|
|
114
|
+
*/
|
|
115
|
+
export function term() {
|
|
116
|
+
return _capabilities;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Re-run capability detection. Tests, `/config` reloads, or runtime env changes.
|
|
121
|
+
*/
|
|
122
|
+
export function refresh() {
|
|
123
|
+
_capabilities = compute();
|
|
124
|
+
return _capabilities;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Listen for terminal resizes. Returns an unsubscribe function.
|
|
129
|
+
* Callers receive the latest capabilities object (with updated columns/rows).
|
|
130
|
+
*/
|
|
131
|
+
export function onResize(handler) {
|
|
132
|
+
if (typeof handler !== 'function') return () => {};
|
|
133
|
+
const stream = process.stdout;
|
|
134
|
+
if (!stream || typeof stream.on !== 'function') return () => {};
|
|
135
|
+
|
|
136
|
+
const onChange = () => {
|
|
137
|
+
_capabilities = {
|
|
138
|
+
..._capabilities,
|
|
139
|
+
columns: stream.columns || _capabilities.columns,
|
|
140
|
+
rows: stream.rows || _capabilities.rows,
|
|
141
|
+
};
|
|
142
|
+
handler(_capabilities);
|
|
143
|
+
};
|
|
144
|
+
stream.on('resize', onChange);
|
|
145
|
+
return () => stream.off('resize', onChange);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Force a capability level. Test-only escape hatch.
|
|
150
|
+
* Pass `null` to clear and re-detect.
|
|
151
|
+
*/
|
|
152
|
+
export function _setForTesting(overrides) {
|
|
153
|
+
if (overrides === null) {
|
|
154
|
+
_capabilities = compute();
|
|
155
|
+
return _capabilities;
|
|
156
|
+
}
|
|
157
|
+
_capabilities = { ..._capabilities, ...overrides };
|
|
158
|
+
return _capabilities;
|
|
159
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text layout helpers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted so the input dock's line-wrapping and clipping logic can be
|
|
5
|
+
* unit-tested without a live terminal. Everything here is a pure function
|
|
6
|
+
* of its inputs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { strip as stripAnsi, width as visibleWidth } from './palette.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wrap `text` into lines of at most `maxWidth` visible columns.
|
|
13
|
+
*
|
|
14
|
+
* - Preserves existing '\n' as hard breaks.
|
|
15
|
+
* - Wraps long lines at whitespace when possible.
|
|
16
|
+
* - Breaks mid-word only when a single word exceeds maxWidth.
|
|
17
|
+
* - Empty input returns [''].
|
|
18
|
+
* - ANSI escapes are preserved; wrap decisions use visible width.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} text
|
|
21
|
+
* @param {number} maxWidth
|
|
22
|
+
* @returns {string[]}
|
|
23
|
+
*/
|
|
24
|
+
export function wrapToLines(text, maxWidth) {
|
|
25
|
+
const width = Math.max(1, Math.floor(maxWidth));
|
|
26
|
+
const source = String(text ?? '');
|
|
27
|
+
if (!source) return [''];
|
|
28
|
+
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const rawLine of source.split('\n')) {
|
|
31
|
+
if (rawLine === '') { out.push(''); continue; }
|
|
32
|
+
if (visibleWidth(rawLine) <= width) { out.push(rawLine); continue; }
|
|
33
|
+
|
|
34
|
+
// Long line — wrap at whitespace where possible.
|
|
35
|
+
let current = '';
|
|
36
|
+
const tokens = tokenizeForWrap(rawLine);
|
|
37
|
+
for (const token of tokens) {
|
|
38
|
+
const isSpace = /^\s+$/.test(stripAnsi(token));
|
|
39
|
+
const candidate = current + token;
|
|
40
|
+
if (visibleWidth(candidate) <= width) { current = candidate; continue; }
|
|
41
|
+
if (current) { out.push(current.replace(/\s+$/, '')); current = ''; }
|
|
42
|
+
if (isSpace) continue; // don't start a new line with pure whitespace
|
|
43
|
+
// Token alone still too wide — chunk it.
|
|
44
|
+
if (visibleWidth(token) > width) {
|
|
45
|
+
for (const chunk of chunkByVisibleWidth(token, width)) out.push(chunk);
|
|
46
|
+
} else {
|
|
47
|
+
current = token;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (current) out.push(current.replace(/\s+$/, ''));
|
|
51
|
+
}
|
|
52
|
+
return out.length ? out : [''];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Take the last `maxRows` of `lines`. If more lines were dropped, prefix
|
|
57
|
+
* the first surviving line with a dim '…' marker so the truncation is
|
|
58
|
+
* explicit (per PRD §5.1).
|
|
59
|
+
*
|
|
60
|
+
* Returns { visible, truncated: boolean, dropped: number }.
|
|
61
|
+
*/
|
|
62
|
+
export function tailWithEllipsis(lines, maxRows, ellipsis = '… ') {
|
|
63
|
+
const rows = Math.max(1, Math.floor(maxRows));
|
|
64
|
+
const arr = Array.isArray(lines) ? lines : [];
|
|
65
|
+
if (arr.length <= rows) return { visible: arr.slice(), truncated: false, dropped: 0 };
|
|
66
|
+
const dropped = arr.length - rows;
|
|
67
|
+
const visible = arr.slice(dropped);
|
|
68
|
+
visible[0] = `${ellipsis}${visible[0] || ''}`;
|
|
69
|
+
return { visible, truncated: true, dropped };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Given the wrapped visible lines and a cursor position (as a linear
|
|
74
|
+
* character offset into the *joined* wrapped output), return the
|
|
75
|
+
* { row, col } inside the visible block (0-indexed).
|
|
76
|
+
*
|
|
77
|
+
* Used to place the terminal cursor after rendering the input buffer.
|
|
78
|
+
*/
|
|
79
|
+
export function cursorPositionInLines(visibleLines, offset) {
|
|
80
|
+
const arr = Array.isArray(visibleLines) ? visibleLines : [];
|
|
81
|
+
let remaining = Math.max(0, Math.floor(offset));
|
|
82
|
+
for (let row = 0; row < arr.length; row++) {
|
|
83
|
+
const line = arr[row] || '';
|
|
84
|
+
const w = visibleWidth(line);
|
|
85
|
+
if (remaining <= w) return { row, col: remaining };
|
|
86
|
+
remaining -= w;
|
|
87
|
+
// Newline between wrapped lines doesn't count as a visible column,
|
|
88
|
+
// but consumes zero of the remaining offset either.
|
|
89
|
+
}
|
|
90
|
+
const lastRow = Math.max(0, arr.length - 1);
|
|
91
|
+
return { row: lastRow, col: visibleWidth(arr[lastRow] || '') };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── internals ────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
// Tokenize into runs of non-whitespace and whitespace so wrapWords can
|
|
97
|
+
// decide break points without merging spaces into words. ANSI escapes stay
|
|
98
|
+
// attached to the visible token they precede.
|
|
99
|
+
function tokenizeForWrap(line) {
|
|
100
|
+
const tokens = [];
|
|
101
|
+
const re = /(\s+|\S+)/g;
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = re.exec(line)) !== null) tokens.push(m[0]);
|
|
104
|
+
return tokens;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Split a single overly-long token into chunks of at most `maxWidth`
|
|
108
|
+
// visible cells, preserving ANSI escape sequences (they don't count).
|
|
109
|
+
function chunkByVisibleWidth(token, maxWidth) {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let buf = '';
|
|
112
|
+
let bufWidth = 0;
|
|
113
|
+
const chars = Array.from(String(token));
|
|
114
|
+
for (const ch of chars) {
|
|
115
|
+
// Naive: treat each codepoint as 1 cell. Good enough for path-like
|
|
116
|
+
// tokens; wide-char inputs would need a fuller width table.
|
|
117
|
+
if (bufWidth + 1 > maxWidth) {
|
|
118
|
+
chunks.push(buf);
|
|
119
|
+
buf = '';
|
|
120
|
+
bufWidth = 0;
|
|
121
|
+
}
|
|
122
|
+
buf += ch;
|
|
123
|
+
bufWidth += 1;
|
|
124
|
+
}
|
|
125
|
+
if (buf) chunks.push(buf);
|
|
126
|
+
return chunks;
|
|
127
|
+
}
|