termux-dev 1.5.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +215 -163
  2. package/assets/banner.svg +23 -28
  3. package/assets/preview.png +0 -0
  4. package/assets/preview.svg +94 -0
  5. package/dist/cli/banner.js +7 -3
  6. package/dist/cli/clipboard-write.js +55 -0
  7. package/dist/cli/clipboard.js +182 -106
  8. package/dist/cli/colors.js +67 -0
  9. package/dist/cli/confirm.js +50 -0
  10. package/dist/cli/doctor.js +4 -4
  11. package/dist/cli/effort.js +80 -0
  12. package/dist/cli/export.js +3 -3
  13. package/dist/cli/files.js +19 -5
  14. package/dist/cli/headless.js +29 -14
  15. package/dist/cli/index.js +1395 -474
  16. package/dist/cli/keys.js +69 -0
  17. package/dist/cli/liveblock.js +45 -0
  18. package/dist/cli/log.js +19 -0
  19. package/dist/cli/markdown.js +158 -36
  20. package/dist/cli/mcp-panel.js +432 -0
  21. package/dist/cli/permission.js +84 -0
  22. package/dist/cli/picker.js +135 -0
  23. package/dist/cli/prompt.js +338 -121
  24. package/dist/cli/prompt_palette.js +6 -0
  25. package/dist/cli/resume.js +15 -0
  26. package/dist/cli/rewind.js +51 -0
  27. package/dist/cli/runtime_errors.js +6 -0
  28. package/dist/cli/server.js +85 -24
  29. package/dist/cli/settings.js +307 -0
  30. package/dist/cli/statusline.js +27 -1
  31. package/dist/cli/theme.js +126 -108
  32. package/dist/cli/transcript.js +114 -0
  33. package/dist/cli/typeahead.js +371 -0
  34. package/dist/cli/ui.js +1021 -0
  35. package/dist/cli/updater.js +23 -21
  36. package/dist/cli/usage-panel.js +242 -0
  37. package/dist/cli/width.js +178 -0
  38. package/dist/core/capabilities.js +433 -0
  39. package/dist/core/catalog-util.js +27 -0
  40. package/dist/core/checkpoint.js +90 -0
  41. package/dist/core/context-stats.js +29 -0
  42. package/dist/core/cost.js +191 -0
  43. package/dist/core/formats.js +199 -0
  44. package/dist/core/history.js +51 -1
  45. package/dist/core/hooks.js +79 -0
  46. package/dist/core/i18n.js +42 -0
  47. package/dist/core/local-config.js +69 -0
  48. package/dist/core/locales/de.js +210 -0
  49. package/dist/core/locales/en.js +221 -0
  50. package/dist/core/locales/es.js +210 -0
  51. package/dist/core/locales/fr.js +210 -0
  52. package/dist/core/locales/ja.js +210 -0
  53. package/dist/core/locales/pt.js +210 -0
  54. package/dist/core/locales/ru.js +210 -0
  55. package/dist/core/locales/uk.js +210 -0
  56. package/dist/core/locales/zh.js +210 -0
  57. package/dist/core/loop.js +125 -49
  58. package/dist/core/memory.js +39 -12
  59. package/dist/core/models.js +33 -20
  60. package/dist/core/network.js +12 -11
  61. package/dist/core/notify.js +54 -0
  62. package/dist/core/planfile.js +43 -0
  63. package/dist/core/price-catalog.json +1 -0
  64. package/dist/core/prices.js +249 -0
  65. package/dist/core/reasoning-catalog.json +1 -0
  66. package/dist/core/reasoning.js +393 -0
  67. package/dist/core/redact.js +41 -0
  68. package/dist/core/secure-file.js +22 -0
  69. package/dist/core/session.js +16 -5
  70. package/dist/core/snapshot.js +14 -2
  71. package/dist/core/title.js +61 -0
  72. package/dist/core/types.js +1 -1
  73. package/dist/core/usage.js +43 -7
  74. package/dist/core/vision-catalog.json +1 -0
  75. package/dist/core/vision.js +123 -0
  76. package/dist/mcp/client.js +36 -5
  77. package/dist/mcp/http-client.js +174 -0
  78. package/dist/mcp/manager.js +330 -93
  79. package/dist/permissions/guard.js +385 -117
  80. package/dist/prompts/builder.js +46 -38
  81. package/dist/providers/anthropic.js +517 -0
  82. package/dist/providers/index.js +18 -1
  83. package/dist/providers/openai.js +138 -125
  84. package/dist/providers/shared.js +116 -0
  85. package/dist/providers/toolguard.js +101 -0
  86. package/dist/tools/bash.js +141 -51
  87. package/dist/tools/diagnostics.js +76 -48
  88. package/dist/tools/fs.js +77 -5
  89. package/dist/tools/index.js +2 -1
  90. package/dist/tools/mcp.js +72 -0
  91. package/dist/tools/packages.js +16 -1
  92. package/dist/tools/plan.js +8 -8
  93. package/dist/tools/questions.js +50 -54
  94. package/dist/tools/server.js +6 -0
  95. package/dist/tools/todo.js +20 -18
  96. package/dist/tools/web.js +143 -8
  97. package/package.json +4 -2
  98. package/assets/preview_work1.png +0 -0
@@ -65,6 +65,32 @@ export function formatCostBadge(cost, maxCostUSD) {
65
65
  }
66
66
  return pc.green(costFormatted);
67
67
  }
68
+ function clipLabel(label, width) {
69
+ if (label.length <= width)
70
+ return label;
71
+ return width > 1 ? `${label.slice(0, width - 1)}…` : label.slice(0, width);
72
+ }
73
+ export function renderCleanStatus(opts) {
74
+ const cols = Math.max(20, opts.cols || process.stdout.columns || 80);
75
+ const available = cols - 2;
76
+ const mode = opts.isYolo ? 'YOLO' : opts.mode;
77
+ const modelWidth = Math.max(4, available - mode.length - 2);
78
+ const shortModel = opts.model.length > modelWidth && opts.model.includes('/')
79
+ ? opts.model.split('/').pop() || opts.model
80
+ : opts.model;
81
+ const model = clipLabel(shortModel, modelWidth);
82
+ const percent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
83
+ const contextLabel = cols < 38 ? 'ctx' : 'context';
84
+ let details = `${contextLabel} ${percent}% · $${opts.cost.toFixed(4)}`;
85
+ if (details.length > available)
86
+ details = `${percent}% · $${opts.cost.toFixed(2)}`;
87
+ const git = GitStatusCache.getStatus();
88
+ const branch = git ? ` · ${git.branch}${git.isDirty ? '*' : ''}` : '';
89
+ if (details.length + branch.length <= available)
90
+ details += branch;
91
+ details = clipLabel(details, available);
92
+ return `${pc.cyan(mode.toLowerCase())} ${pc.bold(model)}\n${pc.dim(details)}`;
93
+ }
68
94
  export function renderPowerlineStatus(opts) {
69
95
  const cols = opts.cols || process.stdout.columns || 80;
70
96
  const theme = getCurrentTheme();
@@ -90,7 +116,7 @@ export function renderPowerlineStatus(opts) {
90
116
  }
91
117
  // Git info
92
118
  const git = GitStatusCache.getStatus();
93
- const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('✱') : ''}` : '';
119
+ const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('*') : ''}` : '';
94
120
  // Context progress
95
121
  const usagePercent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
96
122
  const barWidth = cols < 60 ? 6 : 10;
package/dist/cli/theme.js CHANGED
@@ -1,113 +1,110 @@
1
1
  import pc from 'picocolors';
2
- export const THEMES = {
3
- cyan: {
4
- id: 'cyan',
5
- name: 'Cyan Cyber',
6
- desc: 'Electric neon cyan & obsidian (Default)',
7
- emoji: '⚡',
8
- colorFn: (s) => pc.cyan(s),
9
- boldFn: (s) => pc.bold(pc.cyan(s)),
10
- accentFn: (s) => pc.blue(s),
11
- badgeFn: (s) => pc.bgCyan(pc.black(s)),
12
- diffAddBg: (s) => pc.bgCyan(pc.black(s)),
13
- diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
14
- hex: '#00f2fe'
15
- },
16
- purple: {
17
- id: 'purple',
18
- name: 'Synthwave Purple',
19
- desc: 'Vibrant neon magenta & violet retro',
20
- emoji: '🟣',
21
- colorFn: (s) => pc.magenta(s),
22
- boldFn: (s) => pc.bold(pc.magenta(s)),
23
- accentFn: (s) => pc.blue(s),
24
- badgeFn: (s) => pc.bgMagenta(pc.black(s)),
25
- diffAddBg: (s) => pc.bgMagenta(pc.black(s)),
26
- diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
27
- hex: '#d946ef'
28
- },
29
- matrix: {
30
- id: 'matrix',
31
- name: 'Matrix Hacker',
32
- desc: 'Classic bright phosphor green terminal',
33
- emoji: '🟢',
34
- colorFn: (s) => pc.green(s),
35
- boldFn: (s) => pc.bold(pc.green(s)),
36
- accentFn: (s) => pc.cyan(s),
37
- badgeFn: (s) => pc.bgGreen(pc.black(s)),
38
- diffAddBg: (s) => pc.bgGreen(pc.black(s)),
39
- diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
40
- hex: '#22c55e'
41
- },
42
- amber: {
43
- id: 'amber',
44
- name: 'Solar Amber',
45
- desc: 'Warm vintage CRT amber gold',
46
- emoji: '🟡',
47
- colorFn: (s) => pc.yellow(s),
48
- boldFn: (s) => pc.bold(pc.yellow(s)),
49
- accentFn: (s) => pc.red(s),
50
- badgeFn: (s) => pc.bgYellow(pc.black(s)),
51
- diffAddBg: (s) => pc.bgYellow(pc.black(s)),
52
- diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
53
- hex: '#f59e0b'
54
- },
55
- crimson: {
56
- id: 'crimson',
57
- name: 'Ruby Crimson',
58
- desc: 'Aggressive cyberpunk scarlet red',
59
- emoji: '🔴',
60
- colorFn: (s) => pc.red(s),
61
- boldFn: (s) => pc.bold(pc.red(s)),
62
- accentFn: (s) => pc.magenta(s),
63
- badgeFn: (s) => pc.bgRed(pc.white(s)),
64
- diffAddBg: (s) => pc.bgRed(pc.white(s)),
65
- diffRemoveBg: (s) => pc.bgMagenta(pc.white(s)),
66
- hex: '#ef4444'
67
- },
68
- monochrome: {
69
- id: 'monochrome',
70
- name: 'Pure Monochrome',
71
- desc: 'Crisp minimal pure white & gray',
72
- emoji: '⚪',
73
- colorFn: (s) => pc.white(s),
74
- boldFn: (s) => pc.bold(pc.white(s)),
75
- accentFn: (s) => pc.dim(s),
76
- badgeFn: (s) => pc.bgWhite(pc.black(s)),
77
- diffAddBg: (s) => pc.bgWhite(pc.black(s)),
78
- diffRemoveBg: (s) => pc.bgBlack(pc.white(s)),
79
- hex: '#ffffff'
2
+ import { bgHex, fgHex } from './colors.js';
3
+ const SPECS = [
4
+ {
5
+ id: 'cyan', name: 'Cyan Cyber', desc: 'Electric neon cyan & obsidian (Default)', aliases: ['blue', 'neon', 'cyber'],
6
+ colors: { accent: '#00f2fe', plan: '#b794f6', ok: '#5fd787', warn: '#ffb454', err: '#ff6b6b', yolo: '#ff8a5c', text: '#dadada', muted: '#bcbcbc', faint: '#8a8a8a', rule: '#767676', shade: '#262626' },
7
+ },
8
+ {
9
+ id: 'purple', name: 'Synthwave Purple', desc: 'Vibrant neon magenta & violet retro', aliases: ['magenta', 'violet', 'pink', 'synthwave'],
10
+ colors: { accent: '#d946ef', plan: '#22d3ee', ok: '#86efac', warn: '#fbbf24', err: '#fb7185', yolo: '#fb923c', text: '#e6e0f0', muted: '#b9b0c9', faint: '#8f869f', rule: '#6d6580', shade: '#221a2e' },
11
+ },
12
+ {
13
+ id: 'matrix', name: 'Matrix Hacker', desc: 'Classic bright phosphor green terminal', aliases: ['green', 'hacker', 'terminal'],
14
+ colors: { accent: '#22c55e', plan: '#a3e635', ok: '#4ade80', warn: '#facc15', err: '#f87171', yolo: '#fb923c', text: '#d4f5dc', muted: '#a7c9b0', faint: '#7fa38a', rule: '#5f7f6a', shade: '#10241a' },
15
+ },
16
+ {
17
+ id: 'amber', name: 'Solar Amber', desc: 'Warm vintage CRT amber gold', aliases: ['yellow', 'gold', 'solar', 'orange'],
18
+ colors: { accent: '#f59e0b', plan: '#fb7185', ok: '#a3e635', warn: '#fde047', err: '#f87171', yolo: '#ef6a3a', text: '#f1e6d0', muted: '#c9b896', faint: '#9f8f70', rule: '#7a6c52', shade: '#2a2012' },
19
+ },
20
+ {
21
+ id: 'crimson', name: 'Ruby Crimson', desc: 'Aggressive cyberpunk scarlet red', aliases: ['red', 'ruby', 'cyberpunk'],
22
+ colors: { accent: '#ef4444', plan: '#f472b6', ok: '#86efac', warn: '#fbbf24', err: '#fca5a5', yolo: '#f59e0b', text: '#f0dede', muted: '#c9b0b0', faint: '#9d8787', rule: '#7a6666', shade: '#2c1618' },
23
+ },
24
+ {
25
+ id: 'monochrome', name: 'Pure Monochrome', desc: 'Crisp minimal pure white & gray', aliases: ['white', 'mono', 'gray', 'grey'],
26
+ colors: { accent: '#f5f5f5', plan: '#a8a8a8', ok: '#bcd9bc', warn: '#e6e6e6', err: '#ff8f8f', yolo: '#ffffff', text: '#e4e4e4', muted: '#b8b8b8', faint: '#8c8c8c', rule: '#6e6e6e', shade: '#262626' },
27
+ },
28
+ {
29
+ id: 'nord', name: 'Nord Frost', desc: 'Arctic frost blues, calm and muted', aliases: ['nordic', 'frost', 'arctic'],
30
+ colors: { accent: '#88c0d0', plan: '#b48ead', ok: '#a3be8c', warn: '#ebcb8b', err: '#e5808b', yolo: '#d08770', text: '#eceff4', muted: '#b6bfd0', faint: '#8791a6', rule: '#616b80', shade: '#2e3440' },
31
+ },
32
+ {
33
+ id: 'dracula', name: 'Dracula', desc: 'Dark purple with vivid pink and green', aliases: ['drac', 'vampire'],
34
+ colors: { accent: '#bd93f9', plan: '#ff79c6', ok: '#50fa7b', warn: '#f1fa8c', err: '#ff6e6e', yolo: '#ffb86c', text: '#f8f8f2', muted: '#c3c7dc', faint: '#8e94b5', rule: '#626a8c', shade: '#282a36' },
35
+ },
36
+ {
37
+ id: 'tokyonight', name: 'Tokyo Night', desc: 'Neon-lit Tokyo night, indigo and blue', aliases: ['tokyo', 'night', 'tokyo-night'],
38
+ colors: { accent: '#7aa2f7', plan: '#bb9af7', ok: '#9ece6a', warn: '#e0af68', err: '#f7768e', yolo: '#ff9e64', text: '#c0caf5', muted: '#a9b1d6', faint: '#7f87b0', rule: '#565f89', shade: '#1a1b26' },
39
+ },
40
+ {
41
+ id: 'gruvbox', name: 'Gruvbox', desc: 'Retro warm earth tones, yellow and orange', aliases: ['gruv', 'retro'],
42
+ colors: { accent: '#fabd2f', plan: '#d3869b', ok: '#b8bb26', warn: '#fe8019', err: '#fb4934', yolo: '#e8742a', text: '#ebdbb2', muted: '#bdae93', faint: '#98897a', rule: '#7c6f64', shade: '#32302f' },
43
+ },
44
+ {
45
+ id: 'catppuccin', name: 'Catppuccin Mocha', desc: 'Soothing pastel mocha, mauve accents', aliases: ['mocha', 'cat', 'catppuccin-mocha'],
46
+ colors: { accent: '#cba6f7', plan: '#89b4fa', ok: '#a6e3a1', warn: '#f9e2af', err: '#f38ba8', yolo: '#fab387', text: '#cdd6f4', muted: '#bac2de', faint: '#9399b2', rule: '#6c7086', shade: '#1e1e2e' },
47
+ },
48
+ {
49
+ id: 'solarized', name: 'Solarized Dark', desc: 'Low-glare teal and olive classic', aliases: ['solarized-dark', 'sol'],
50
+ colors: { accent: '#2aa198', plan: '#8f94dc', ok: '#9bb000', warn: '#d9a21b', err: '#ee6a67', yolo: '#ea7b4b', text: '#bcc8c8', muted: '#9eb0b2', faint: '#7f9aa0', rule: '#586e75', shade: '#0b2a33' },
51
+ },
52
+ {
53
+ id: 'rosepine', name: 'Rose Pine', desc: 'Soft rose and gold on dusky violet', aliases: ['rose', 'pine', 'rose-pine'],
54
+ colors: { accent: '#ebbcba', plan: '#c4a7e7', ok: '#9ccfd8', warn: '#f6c177', err: '#eb6f92', yolo: '#ea9a97', text: '#e0def4', muted: '#c1bed6', faint: '#908caa', rule: '#6e6a86', shade: '#1f1d2e' },
55
+ },
56
+ {
57
+ id: 'monokai', name: 'Monokai', desc: 'Bold lime and magenta editor classic', aliases: ['mono-kai'],
58
+ colors: { accent: '#a6e22e', plan: '#ae81ff', ok: '#a6e22e', warn: '#e6db74', err: '#f92672', yolo: '#fd971f', text: '#f8f8f2', muted: '#cfcfc2', faint: '#9a9a8e', rule: '#75715e', shade: '#272822' },
59
+ },
60
+ {
61
+ id: 'github', name: 'GitHub Dark', desc: 'Clean blue and green, easy on the eyes', aliases: ['gh', 'git'],
62
+ colors: { accent: '#58a6ff', plan: '#bc8cff', ok: '#3fb950', warn: '#d29922', err: '#f85149', yolo: '#e3742f', text: '#e6edf3', muted: '#b1bac4', faint: '#8b949e', rule: '#6e7681', shade: '#161b22' },
63
+ },
64
+ {
65
+ id: 'ocean', name: 'Deep Ocean', desc: 'Deep sea cyan and indigo', aliases: ['sea', 'deepsea'],
66
+ colors: { accent: '#38bdf8', plan: '#818cf8', ok: '#34d399', warn: '#fbbf24', err: '#fb7185', yolo: '#fb923c', text: '#dbeafe', muted: '#a9c3dd', faint: '#7f9dba', rule: '#55708c', shade: '#0c1a2b' },
67
+ },
68
+ {
69
+ id: 'sunset', name: 'Sunset Glow', desc: 'Coral and violet dusk gradient', aliases: ['dusk', 'coral'],
70
+ colors: { accent: '#ff7e67', plan: '#c084fc', ok: '#86efac', warn: '#fcd34d', err: '#f87171', yolo: '#fb923c', text: '#f5e6e0', muted: '#d1b8b0', faint: '#a69089', rule: '#806560', shade: '#2b1621' },
71
+ },
72
+ {
73
+ id: 'forest', name: 'Forest Moss', desc: 'Mossy greens and woodland earth', aliases: ['moss', 'woods'],
74
+ colors: { accent: '#8fbc6a', plan: '#d4a373', ok: '#a3d977', warn: '#e9c46a', err: '#ef8a72', yolo: '#f4a261', text: '#e5e8d8', muted: '#b9c0a6', faint: '#8f987b', rule: '#66705a', shade: '#1c2418' },
75
+ },
76
+ ];
77
+ const bold = (s) => (pc.isColorSupported ? `\x1b[1m${s}\x1b[22m` : s);
78
+ function buildTheme(spec, options = {}) {
79
+ const c = spec.colors;
80
+ const tokens = {};
81
+ for (const key of Object.keys(c)) {
82
+ tokens[key] = key === 'shade' ? bgHex(c[key], options) : fgHex(c[key], options);
80
83
  }
81
- };
82
- const THEME_ALIASES = {
83
- '1': 'cyan',
84
- '2': 'purple',
85
- '3': 'matrix',
86
- '4': 'amber',
87
- '5': 'crimson',
88
- '6': 'monochrome',
89
- 'blue': 'cyan',
90
- 'neon': 'cyan',
91
- 'cyber': 'cyan',
92
- 'magenta': 'purple',
93
- 'violet': 'purple',
94
- 'pink': 'purple',
95
- 'synthwave': 'purple',
96
- 'green': 'matrix',
97
- 'hacker': 'matrix',
98
- 'terminal': 'matrix',
99
- 'yellow': 'amber',
100
- 'gold': 'amber',
101
- 'solar': 'amber',
102
- 'orange': 'amber',
103
- 'red': 'crimson',
104
- 'ruby': 'crimson',
105
- 'cyberpunk': 'crimson',
106
- 'white': 'monochrome',
107
- 'mono': 'monochrome',
108
- 'gray': 'monochrome',
109
- 'grey': 'monochrome'
110
- };
84
+ const black = fgHex('#000000', options);
85
+ return {
86
+ id: spec.id,
87
+ name: spec.name,
88
+ desc: spec.desc,
89
+ symbol: '●',
90
+ hex: c.accent,
91
+ colors: c,
92
+ tokens,
93
+ colorFn: tokens.accent,
94
+ boldFn: (s) => bold(tokens.accent(s)),
95
+ accentFn: tokens.plan,
96
+ badgeFn: (s) => bgHex(c.accent, options)(black(s)),
97
+ diffAddBg: (s) => bgHex(c.ok, options)(black(s)),
98
+ diffRemoveBg: (s) => bgHex(c.err, options)(black(s)),
99
+ };
100
+ }
101
+ export const THEMES = Object.fromEntries(SPECS.map((spec) => [spec.id, buildTheme(spec)]));
102
+ const THEME_ALIASES = {};
103
+ SPECS.forEach((spec, index) => {
104
+ THEME_ALIASES[String(index + 1)] = spec.id; // "1" ... "18"
105
+ for (const alias of spec.aliases ?? [])
106
+ THEME_ALIASES[alias] = spec.id;
107
+ });
111
108
  let activeThemeId = 'cyan';
112
109
  export function getTheme(themeId) {
113
110
  const id = (themeId || activeThemeId).toLowerCase().trim();
@@ -138,3 +135,24 @@ export function getCurrentTheme() {
138
135
  export function listThemes() {
139
136
  return Object.values(THEMES);
140
137
  }
138
+ /** Four dots showing the palette at a glance: accent, plan, ok, warn. */
139
+ export function renderSwatch(theme) {
140
+ return [theme.tokens.accent, theme.tokens.plan, theme.tokens.ok, theme.tokens.warn].map((paint) => paint(theme.symbol)).join(' ');
141
+ }
142
+ /** A small sample screen painted with the theme, shown while choosing one. */
143
+ export function renderThemePreview(theme) {
144
+ const k = theme.tokens;
145
+ const modes = [k.accent(`${theme.symbol} agent`), k.plan('◆ plan'), k.yolo('▲ yolo')].join(' ');
146
+ const states = [k.ok('✓ done'), k.warn('! careful'), k.err('✗ failed')].join(' ');
147
+ const text = [k.text('text'), k.muted('muted'), k.faint('faint'), k.rule('─────')].join(' ');
148
+ const message = k.shade(` ${k.accent('❯')} ${k.text('explain this project')} `);
149
+ return [modes, states, text, message].join('\n');
150
+ }
151
+ /** Builds a theme with explicit colour options (tests, forced 256-colour output). */
152
+ export function buildThemeFor(id, options) {
153
+ const spec = SPECS.find((s) => s.id === id);
154
+ if (!spec)
155
+ throw new Error(`Unknown theme: ${id}`);
156
+ return buildTheme(spec, options);
157
+ }
158
+ export const THEME_IDS = SPECS.map((s) => s.id);
@@ -0,0 +1,114 @@
1
+ import { getActionDescription } from '../core/loop.js';
2
+ import { formatDiffBox } from './diff.js';
3
+ import { renderMarkdown } from './markdown.js';
4
+ import { palette, renderPlanCard, renderResultLine, renderTaskCard, renderToolLine, renderUserEcho, summarizeToolResult } from './ui.js';
5
+ import { displayWidth, truncateWidth, wrapText } from './width.js';
6
+ import { t } from '../core/i18n.js';
7
+ const MAX_ECHO_LINES = 300;
8
+ /** The rows under a tool line: a plan card, a diff, or the output (one row folded, all of it unfolded). */
9
+ export function toolResultRows(name, result, opts) {
10
+ const { cols } = opts;
11
+ try {
12
+ const parsed = JSON.parse(result);
13
+ if (parsed && typeof parsed === 'object') {
14
+ if (parsed.type === 'todo_list' && Array.isArray(parsed.tasks) && parsed.tasks.length > 0)
15
+ return renderTaskCard(parsed.tasks, cols);
16
+ if (parsed.type === 'plan_ready' && typeof parsed.summary === 'string')
17
+ return renderPlanCard({ summary: parsed.summary, files: parsed.files, steps: parsed.steps }, cols);
18
+ if (parsed.action === 'edit' && parsed.diffLines) {
19
+ const box = formatDiffBox(parsed.path, parsed.diffLines, Math.min(cols - 4, 80), undefined, !!opts.diffColors);
20
+ const rows = box.split('\n').map((l) => ' ' + l);
21
+ if (parsed.summary)
22
+ rows.push(renderResultLine(parsed.summary, 'ok', cols));
23
+ return rows;
24
+ }
25
+ if (parsed.summary)
26
+ return [renderResultLine(String(parsed.summary), 'ok', cols)];
27
+ }
28
+ }
29
+ catch { }
30
+ return summarizeToolResult(name, result, cols, opts.expanded);
31
+ }
32
+ export function assistantRows(text) {
33
+ const rows = renderMarkdown(text).replace(/\s+$/, '').split('\n');
34
+ return rows.map((row, i) => (i === 0 ? ' ' + palette.accent('●') + ' ' : ' ') + row);
35
+ }
36
+ /** ` ∴ thought for 0.3s · 112 tok (ctrl+p to expand)`, the hint dropped and the rest cut when the screen is narrow. */
37
+ export function collapsedThoughtLine(summary, hasText, cols) {
38
+ const room = Math.max(10, cols - 1 - 4);
39
+ const hint = hasText ? ` (${t('view.expandThought')})` : '';
40
+ const text = displayWidth(summary + hint) <= room ? summary + hint : truncateWidth(summary, room);
41
+ return palette.muted(' ∴ ') + palette.faint(text);
42
+ }
43
+ function thoughtRows(item, opts) {
44
+ const summary = `${t('status.thoughtFor', { sec: item.sec })}${item.stats}`;
45
+ if (!opts.thinkingExpanded || !item.text.trim()) {
46
+ return [collapsedThoughtLine(summary, !!item.text.trim(), opts.cols)];
47
+ }
48
+ const bar = palette.faint(' │ ');
49
+ const body = item.text.trim().split('\n').flatMap((line) => (line.trim() === '' ? [''] : wrapText(line, Math.max(20, opts.cols - 8))));
50
+ return [palette.muted(` ∴ ${t('status.thinkingLine')}`), ...body.map((l) => bar + palette.muted(l)), palette.faint(` ╰ ${summary}`)];
51
+ }
52
+ /** One item as terminal rows (without the blank row that separates it from its neighbour). */
53
+ export function renderItem(item, opts) {
54
+ switch (item.kind) {
55
+ case 'user': {
56
+ const lines = item.text.replace(/\t/g, ' ').split('\n');
57
+ const text = lines.length > MAX_ECHO_LINES ? [...lines.slice(0, MAX_ECHO_LINES), `… +${lines.length - MAX_ECHO_LINES} lines`].join('\n') : item.text;
58
+ return renderUserEcho(text, opts.cols);
59
+ }
60
+ case 'thought':
61
+ return thoughtRows(item, opts);
62
+ case 'assistant':
63
+ return assistantRows(item.text);
64
+ case 'tool': {
65
+ const rows = renderToolLine(item.desc, opts.cols, opts.outputExpanded).split('\n');
66
+ if (item.result !== undefined)
67
+ rows.push(...toolResultRows(item.name, item.result, { cols: opts.cols, expanded: opts.outputExpanded, diffColors: opts.diffColors }));
68
+ return rows;
69
+ }
70
+ default:
71
+ return item.rows;
72
+ }
73
+ }
74
+ /** Whether a blank row goes between two neighbours: everywhere, except between two tool calls and after the user's own padded echo. */
75
+ export function gapBetween(prev, next) {
76
+ if (!prev)
77
+ return false;
78
+ if (prev.kind === 'user' || next.kind === 'user')
79
+ return false;
80
+ if (prev.kind === 'tool' && next.kind === 'tool')
81
+ return false;
82
+ return true;
83
+ }
84
+ export function renderTranscript(items, opts) {
85
+ const out = [];
86
+ items.forEach((item, i) => {
87
+ if (gapBetween(items[i - 1], item))
88
+ out.push('');
89
+ out.push(...renderItem(item, opts));
90
+ });
91
+ return out;
92
+ }
93
+ /** A saved conversation as items (a resumed session has no reasoning texts: they were never stored). */
94
+ export function messagesToItems(messages) {
95
+ const results = new Map();
96
+ for (const m of messages)
97
+ if (m.role === 'tool' && m.tool_call_id)
98
+ results.set(m.tool_call_id, m);
99
+ const items = [];
100
+ for (const m of messages) {
101
+ if (m.role === 'user')
102
+ items.push({ kind: 'user', text: m.content.split('\n\n--- Attached Files ---')[0] });
103
+ else if (m.role === 'assistant') {
104
+ if (m.reasoning && m.reasoning.text.trim())
105
+ items.push({ kind: 'thought', text: m.reasoning.text, sec: m.reasoning.sec, stats: m.reasoning.stats });
106
+ if (m.content && m.content.trim())
107
+ items.push({ kind: 'assistant', text: m.content });
108
+ for (const call of m.tool_calls ?? []) {
109
+ items.push({ kind: 'tool', name: call.name, desc: getActionDescription(call.name, call.arguments ?? {}), result: results.get(call.id)?.content });
110
+ }
111
+ }
112
+ }
113
+ return items;
114
+ }