@compilr-dev/cli 0.6.6 → 0.7.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.
- package/dist/commands-v2/handlers/core.js +10 -1
- package/dist/commands-v2/handlers/index.d.ts +1 -0
- package/dist/commands-v2/handlers/index.js +3 -0
- package/dist/commands-v2/handlers/settings.js +21 -5
- package/dist/commands-v2/handlers/skill.d.ts +10 -0
- package/dist/commands-v2/handlers/skill.js +63 -0
- package/dist/commands-v2/index.d.ts +1 -1
- package/dist/commands-v2/index.js +1 -1
- package/dist/commands-v2/registry.d.ts +4 -0
- package/dist/commands-v2/registry.js +19 -0
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/index.js +8 -12
- package/dist/repl-helpers.d.ts +29 -1
- package/dist/repl-helpers.js +77 -7
- package/dist/repl-v2.js +29 -3
- package/dist/tools/delegation-status.d.ts +3 -12
- package/dist/tools/delegation-status.js +4 -123
- package/dist/tools/handoff.d.ts +9 -17
- package/dist/tools/handoff.js +28 -48
- package/dist/ui/conversation.js +1 -2
- package/dist/ui/markdown-renderer.d.ts +43 -0
- package/dist/ui/markdown-renderer.js +474 -0
- package/dist/ui/overlay/impl/artifact-detail-overlay-v2.js +1 -2
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1 -2
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +7 -1
- package/dist/ui/overlay/impl/help-overlay-v2.js +19 -2
- package/dist/ui/overlay/impl/skill-detail-overlay-v2.d.ts +91 -0
- package/dist/ui/overlay/impl/skill-detail-overlay-v2.js +863 -0
- package/dist/ui/overlay/impl/skill-editor-overlay.d.ts +56 -0
- package/dist/ui/overlay/impl/skill-editor-overlay.js +493 -0
- package/dist/ui/overlay/impl/skills-overlay-v2.d.ts +83 -0
- package/dist/ui/overlay/impl/skills-overlay-v2.js +1095 -0
- package/dist/utils/skill-paths.d.ts +21 -0
- package/dist/utils/skill-paths.js +44 -0
- package/package.json +9 -6
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Detail Overlay V2
|
|
3
|
+
*
|
|
4
|
+
* Fullscreen SKILL.md viewer AND editor with vim-style navigation.
|
|
5
|
+
* Copied from DocumentDetailOverlayV2 — adapted for file-based skills.
|
|
6
|
+
*
|
|
7
|
+
* Modes:
|
|
8
|
+
* - View: rendered markdown, scrollable (j/k, g/G, PgUp/PgDn)
|
|
9
|
+
* - Edit: raw markdown editing with cursor, line numbers, syntax highlighting
|
|
10
|
+
* - Diff: side-by-side diff against upstream SDK version (forked skills only)
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Full-screen viewing with markdown rendering
|
|
14
|
+
* - Vim-style navigation
|
|
15
|
+
* - Edit mode with raw markdown editing
|
|
16
|
+
* - Save to disk (Ctrl+S) with validation warnings
|
|
17
|
+
* - Dirty check with save prompt on exit
|
|
18
|
+
* - Diff view for forked skills (D key)
|
|
19
|
+
*/
|
|
20
|
+
import { promises as fsAsync } from 'node:fs';
|
|
21
|
+
import chalk from 'chalk';
|
|
22
|
+
import hljs from 'highlight.js';
|
|
23
|
+
import { ttyWrite } from '../../terminal.js';
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Alternate Screen Buffer Management
|
|
26
|
+
// =============================================================================
|
|
27
|
+
let inAlternateScreen = false;
|
|
28
|
+
function enterAlternateScreen() {
|
|
29
|
+
if (inAlternateScreen)
|
|
30
|
+
return;
|
|
31
|
+
ttyWrite('\x1b[?1049h');
|
|
32
|
+
ttyWrite('\x1b[H');
|
|
33
|
+
inAlternateScreen = true;
|
|
34
|
+
}
|
|
35
|
+
function exitAlternateScreen() {
|
|
36
|
+
if (!inAlternateScreen)
|
|
37
|
+
return;
|
|
38
|
+
ttyWrite('\x1b[?1049l');
|
|
39
|
+
inAlternateScreen = false;
|
|
40
|
+
}
|
|
41
|
+
function setupAlternateScreenCleanup() {
|
|
42
|
+
const cleanup = () => { exitAlternateScreen(); };
|
|
43
|
+
process.on('exit', cleanup);
|
|
44
|
+
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
45
|
+
if (process.platform !== 'win32') {
|
|
46
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
47
|
+
}
|
|
48
|
+
process.on('uncaughtException', (err) => { cleanup(); console.error('Uncaught exception:', err); process.exit(1); });
|
|
49
|
+
process.on('unhandledRejection', (reason) => { cleanup(); console.error('Unhandled rejection:', reason); process.exit(1); });
|
|
50
|
+
}
|
|
51
|
+
setupAlternateScreenCleanup();
|
|
52
|
+
import { marked } from 'marked';
|
|
53
|
+
import { markedTerminal } from '../../markdown-renderer.js';
|
|
54
|
+
import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
|
|
55
|
+
import { renderBorder, isCtrlC, isClose, isNavigateUp, isNavigateDown, isUpArrow, isDownArrow, isLeftArrow, isRightArrow, isHome, isEnd, isPageUp, isPageDown, isSpace, isBackspace, isDelete, isEnter, isTab, isEscape, isE, getPrintableChar, } from '../../base/index.js';
|
|
56
|
+
import { getCurrentTheme } from '../../../themes/index.js';
|
|
57
|
+
import * as terminal from '../../terminal.js';
|
|
58
|
+
import { parseSkillMarkdown, diffForkVsUpstream, validateSkillQuality, } from '@compilr-dev/sdk';
|
|
59
|
+
import { highlight as cliHighlight, supportsLanguage } from 'cli-highlight';
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Syntax Highlighting (from document-detail-overlay-v2.ts)
|
|
62
|
+
// =============================================================================
|
|
63
|
+
const languageAliases = {
|
|
64
|
+
'sh': 'bash', 'zsh': 'bash', 'fish': 'bash',
|
|
65
|
+
'console': 'bash', 'terminal': 'bash', 'command': 'bash',
|
|
66
|
+
'jsx': 'javascript', 'tsx': 'typescript',
|
|
67
|
+
'htm': 'xml', 'html': 'xml', 'vue': 'xml', 'svelte': 'xml',
|
|
68
|
+
'text': 'plaintext', 'txt': 'plaintext', 'log': 'plaintext',
|
|
69
|
+
'output': 'plaintext', 'result': 'plaintext',
|
|
70
|
+
'jsonc': 'json', 'json5': 'json', 'jsonl': 'json',
|
|
71
|
+
'conf': 'ini', 'config': 'ini', 'env': 'ini', 'dotenv': 'ini',
|
|
72
|
+
};
|
|
73
|
+
for (const [alias, target] of Object.entries(languageAliases)) {
|
|
74
|
+
try {
|
|
75
|
+
hljs.registerAliases(alias, { languageName: target });
|
|
76
|
+
}
|
|
77
|
+
catch { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// Markdown Rendering
|
|
81
|
+
// =============================================================================
|
|
82
|
+
function getThemedMarkedOptions(termWidth) {
|
|
83
|
+
const theme = getCurrentTheme();
|
|
84
|
+
const primary = chalk.hex(theme.colors.primary);
|
|
85
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
86
|
+
const fg = chalk.hex(theme.colors.foreground);
|
|
87
|
+
const muted = chalk.hex(theme.colors.muted);
|
|
88
|
+
const border = chalk.hex(theme.colors.border);
|
|
89
|
+
return {
|
|
90
|
+
reflowText: true,
|
|
91
|
+
width: Math.min(100, termWidth - 4),
|
|
92
|
+
unescape: true,
|
|
93
|
+
tab: 2,
|
|
94
|
+
showSectionPrefix: false,
|
|
95
|
+
heading: primary.bold,
|
|
96
|
+
firstHeading: primary.bold,
|
|
97
|
+
strong: fg.bold,
|
|
98
|
+
em: fg.italic,
|
|
99
|
+
link: secondary,
|
|
100
|
+
href: secondary.underline,
|
|
101
|
+
blockquote: muted.italic,
|
|
102
|
+
del: muted.strikethrough,
|
|
103
|
+
hr: border,
|
|
104
|
+
codespan: primary,
|
|
105
|
+
code: secondary,
|
|
106
|
+
html: muted,
|
|
107
|
+
table: fg,
|
|
108
|
+
listitem: fg,
|
|
109
|
+
paragraph: fg,
|
|
110
|
+
tableOptions: {
|
|
111
|
+
style: { head: ['bold'], border: [] },
|
|
112
|
+
chars: {
|
|
113
|
+
'top': '─', 'top-mid': '─', 'top-left': '─', 'top-right': '─',
|
|
114
|
+
'bottom': '─', 'bottom-mid': '─', 'bottom-left': '─', 'bottom-right': '─',
|
|
115
|
+
'left': ' ', 'left-mid': ' ', 'mid': '─', 'mid-mid': '─',
|
|
116
|
+
'right': ' ', 'right-mid': ' ', 'middle': ' ',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function renderMarkdownSync(content, termWidth) {
|
|
122
|
+
const options = getThemedMarkedOptions(termWidth);
|
|
123
|
+
marked.use(markedTerminal(options));
|
|
124
|
+
const rendered = marked.parse(content, { async: false });
|
|
125
|
+
return rendered.split('\n');
|
|
126
|
+
}
|
|
127
|
+
function highlightEditLine(line, codeBlockState, theme) {
|
|
128
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
129
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)\s*$/);
|
|
130
|
+
if (fenceMatch) {
|
|
131
|
+
const [, fence, lang] = fenceMatch;
|
|
132
|
+
const fenceChar = fence[0];
|
|
133
|
+
const fenceLength = fence.length;
|
|
134
|
+
if (!codeBlockState.inCodeBlock) {
|
|
135
|
+
return {
|
|
136
|
+
highlighted: secondary(line),
|
|
137
|
+
newState: { inCodeBlock: true, language: lang || 'plaintext', fenceChar, fenceLength },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else if (fenceChar === codeBlockState.fenceChar && fenceLength >= codeBlockState.fenceLength) {
|
|
141
|
+
return {
|
|
142
|
+
highlighted: secondary(line),
|
|
143
|
+
newState: { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (codeBlockState.inCodeBlock) {
|
|
148
|
+
return {
|
|
149
|
+
highlighted: highlightCodeLine(line, codeBlockState.language, theme),
|
|
150
|
+
newState: codeBlockState,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { highlighted: highlightMarkdownLine(line, theme), newState: codeBlockState };
|
|
154
|
+
}
|
|
155
|
+
function highlightCodeLine(line, language, theme) {
|
|
156
|
+
const langMap = {
|
|
157
|
+
'js': 'javascript', 'ts': 'typescript', 'py': 'python', 'rb': 'ruby',
|
|
158
|
+
'sh': 'bash', 'shell': 'bash', 'zsh': 'bash', 'yml': 'yaml', 'md': 'markdown',
|
|
159
|
+
'jsx': 'javascript', 'tsx': 'typescript',
|
|
160
|
+
};
|
|
161
|
+
const mappedLang = langMap[language.toLowerCase()] || language.toLowerCase();
|
|
162
|
+
if (!supportsLanguage(mappedLang) && mappedLang !== 'plaintext') {
|
|
163
|
+
return chalk.hex(theme.colors.secondary || theme.colors.primary)(line);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
if (mappedLang === 'plaintext')
|
|
167
|
+
return chalk.hex(theme.colors.secondary || theme.colors.primary)(line);
|
|
168
|
+
return cliHighlight(line, { language: mappedLang, ignoreIllegals: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return chalk.hex(theme.colors.secondary || theme.colors.primary)(line);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function highlightMarkdownLine(line, theme) {
|
|
175
|
+
const primary = chalk.hex(theme.colors.primary);
|
|
176
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
177
|
+
const muted = chalk.hex(theme.colors.muted);
|
|
178
|
+
if (!line.trim())
|
|
179
|
+
return line;
|
|
180
|
+
// YAML frontmatter delimiter
|
|
181
|
+
if (line === '---')
|
|
182
|
+
return muted(line);
|
|
183
|
+
// Headers
|
|
184
|
+
const headerMatch = line.match(/^(#{1,6})\s(.*)$/);
|
|
185
|
+
if (headerMatch)
|
|
186
|
+
return primary(headerMatch[1]) + ' ' + primary.bold(headerMatch[2]);
|
|
187
|
+
// Horizontal rule
|
|
188
|
+
if (line.match(/^([-*_])\1{2,}\s*$/))
|
|
189
|
+
return muted(line);
|
|
190
|
+
// Unordered list
|
|
191
|
+
const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
192
|
+
if (ulMatch)
|
|
193
|
+
return ulMatch[1] + secondary(ulMatch[2]) + ' ' + ulMatch[3];
|
|
194
|
+
// Ordered list
|
|
195
|
+
const olMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
196
|
+
if (olMatch)
|
|
197
|
+
return olMatch[1] + secondary(olMatch[2]) + ' ' + olMatch[3];
|
|
198
|
+
// Blockquote
|
|
199
|
+
const bqMatch = line.match(/^(>\s*)(.*)$/);
|
|
200
|
+
if (bqMatch)
|
|
201
|
+
return muted(bqMatch[1]) + muted.italic(bqMatch[2]);
|
|
202
|
+
// YAML key: value (inside frontmatter)
|
|
203
|
+
const kvMatch = line.match(/^(\s*)([\w-]+)(\s*:\s*)(.*)/);
|
|
204
|
+
if (kvMatch)
|
|
205
|
+
return kvMatch[1] + primary(kvMatch[2]) + muted(kvMatch[3]) + secondary(kvMatch[4]);
|
|
206
|
+
return line;
|
|
207
|
+
}
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// Key helpers
|
|
210
|
+
// =============================================================================
|
|
211
|
+
function isCtrlS(data) {
|
|
212
|
+
return data.length === 1 && data[0] === 0x13;
|
|
213
|
+
}
|
|
214
|
+
function isEditKey(data) {
|
|
215
|
+
return isE(data) || (data.length === 1 && data[0] === 0x69); // 'i'
|
|
216
|
+
}
|
|
217
|
+
const TAB_SIZE = 2;
|
|
218
|
+
// =============================================================================
|
|
219
|
+
// Skill Detail Overlay
|
|
220
|
+
// =============================================================================
|
|
221
|
+
export class SkillDetailOverlayV2 extends BaseOverlayV2 {
|
|
222
|
+
type = 'inline';
|
|
223
|
+
id = 'skill-detail-overlay-v2';
|
|
224
|
+
usesAlternateScreen = true;
|
|
225
|
+
filePath;
|
|
226
|
+
constructor(options) {
|
|
227
|
+
const termWidth = terminal.getTerminalWidth();
|
|
228
|
+
// Render body only (skip frontmatter for view mode)
|
|
229
|
+
const bodyContent = extractBody(options.content);
|
|
230
|
+
const contentLines = renderMarkdownSync(bodyContent, termWidth);
|
|
231
|
+
const rawLines = options.content.split('\n');
|
|
232
|
+
// Validate
|
|
233
|
+
const parsed = parseSkillMarkdown(options.content);
|
|
234
|
+
const warnings = parsed
|
|
235
|
+
? validateSkillQuality(parsed, options.skillName).map((i) => i.message)
|
|
236
|
+
: ['Invalid frontmatter — required fields: name, description.'];
|
|
237
|
+
// Compute diff if forked
|
|
238
|
+
let diffLines = null;
|
|
239
|
+
if (options.forkedFrom && parsed?.prompt) {
|
|
240
|
+
diffLines = diffForkVsUpstream(parsed.prompt, options.forkedFrom.skill);
|
|
241
|
+
}
|
|
242
|
+
super({
|
|
243
|
+
mode: 'view',
|
|
244
|
+
contentLines,
|
|
245
|
+
scrollOffset: 0,
|
|
246
|
+
rawLines,
|
|
247
|
+
cursorLine: 0,
|
|
248
|
+
cursorColumn: 0,
|
|
249
|
+
editScrollOffset: 0,
|
|
250
|
+
isDirty: false,
|
|
251
|
+
originalContent: options.content,
|
|
252
|
+
showSavePrompt: false,
|
|
253
|
+
saveMessage: null,
|
|
254
|
+
diffLines,
|
|
255
|
+
diffScrollOffset: 0,
|
|
256
|
+
skillName: options.skillName,
|
|
257
|
+
scope: options.scope,
|
|
258
|
+
forkedFrom: options.forkedFrom ?? null,
|
|
259
|
+
boundCommands: options.boundCommands ?? [],
|
|
260
|
+
validationWarnings: warnings,
|
|
261
|
+
});
|
|
262
|
+
this.filePath = options.filePath;
|
|
263
|
+
}
|
|
264
|
+
onMount() { enterAlternateScreen(); }
|
|
265
|
+
onUnmount() { exitAlternateScreen(); }
|
|
266
|
+
// ===========================================================================
|
|
267
|
+
// Render dispatch
|
|
268
|
+
// ===========================================================================
|
|
269
|
+
renderContent(context) {
|
|
270
|
+
if (this.state.mode === 'edit')
|
|
271
|
+
return this.renderEditMode(context);
|
|
272
|
+
if (this.state.mode === 'diff')
|
|
273
|
+
return this.renderDiffMode(context);
|
|
274
|
+
return this.renderViewMode(context);
|
|
275
|
+
}
|
|
276
|
+
// ===========================================================================
|
|
277
|
+
// View Mode
|
|
278
|
+
// ===========================================================================
|
|
279
|
+
renderViewMode(context) {
|
|
280
|
+
const s = context.styles;
|
|
281
|
+
const cols = context.width;
|
|
282
|
+
const rows = context.height;
|
|
283
|
+
const border = renderBorder(cols, s);
|
|
284
|
+
const lines = [];
|
|
285
|
+
// Header
|
|
286
|
+
lines.push(border);
|
|
287
|
+
const scopeLabel = this.state.scope === 'project' ? s.secondary('[project]') : s.muted('[user]');
|
|
288
|
+
let headerExtra = ` ${scopeLabel}`;
|
|
289
|
+
if (this.state.boundCommands.length > 0) {
|
|
290
|
+
headerExtra += ` ${s.muted('→')} ${s.primary(this.state.boundCommands.join(', '))}`;
|
|
291
|
+
}
|
|
292
|
+
if (this.state.forkedFrom) {
|
|
293
|
+
headerExtra += ` ${s.muted(`forked from ${this.state.forkedFrom.skill}`)}`;
|
|
294
|
+
}
|
|
295
|
+
lines.push(` ${s.primaryBold('SKILL')}${s.muted(' │ ')}${chalk.bold(this.state.skillName)}${headerExtra}`);
|
|
296
|
+
lines.push(border);
|
|
297
|
+
// Validation warnings
|
|
298
|
+
if (this.state.validationWarnings.length > 0) {
|
|
299
|
+
lines.push(` ${s.warning(`⚠ ${String(this.state.validationWarnings.length)} warning${this.state.validationWarnings.length > 1 ? 's' : ''}`)}`);
|
|
300
|
+
}
|
|
301
|
+
// Content area
|
|
302
|
+
const headerUsed = lines.length;
|
|
303
|
+
const footerLines = 3;
|
|
304
|
+
const contentHeight = rows - headerUsed - footerLines;
|
|
305
|
+
const totalLines = this.state.contentLines.length;
|
|
306
|
+
const visibleLines = [];
|
|
307
|
+
let physicalLinesUsed = 0;
|
|
308
|
+
for (let i = this.state.scrollOffset; i < totalLines && physicalLinesUsed < contentHeight; i++) {
|
|
309
|
+
const line = this.state.contentLines[i];
|
|
310
|
+
// eslint-disable-next-line no-control-regex
|
|
311
|
+
const visualLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
312
|
+
const linePhysicalHeight = Math.max(1, Math.ceil(visualLen / cols));
|
|
313
|
+
if (physicalLinesUsed + linePhysicalHeight <= contentHeight) {
|
|
314
|
+
visibleLines.push(line);
|
|
315
|
+
physicalLinesUsed += linePhysicalHeight;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const line of visibleLines)
|
|
322
|
+
lines.push(line);
|
|
323
|
+
for (let i = physicalLinesUsed; i < contentHeight; i++)
|
|
324
|
+
lines.push('');
|
|
325
|
+
// Footer
|
|
326
|
+
const canScrollUp = this.state.scrollOffset > 0;
|
|
327
|
+
const canScrollDown = this.state.scrollOffset + contentHeight < totalLines;
|
|
328
|
+
const arrows = (canScrollUp ? '▲' : ' ') + (canScrollDown ? '▼' : ' ');
|
|
329
|
+
const position = totalLines > 0
|
|
330
|
+
? `Line ${String(this.state.scrollOffset + 1)}-${String(Math.min(this.state.scrollOffset + contentHeight, totalLines))} of ${String(totalLines)}`
|
|
331
|
+
: '';
|
|
332
|
+
const scrollPercent = totalLines > contentHeight
|
|
333
|
+
? Math.round(((this.state.scrollOffset + contentHeight) / totalLines) * 100)
|
|
334
|
+
: 100;
|
|
335
|
+
const diffHint = this.state.diffLines ? ' · D Diff' : '';
|
|
336
|
+
lines.push(border);
|
|
337
|
+
lines.push(s.muted(` ${arrows} ${position} (${String(scrollPercent)}%) ↑↓/jk Scroll · e/i Edit${diffHint} · q/Esc Back`));
|
|
338
|
+
lines.push(border);
|
|
339
|
+
return lines;
|
|
340
|
+
}
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
// Edit Mode
|
|
343
|
+
// ===========================================================================
|
|
344
|
+
renderEditMode(context) {
|
|
345
|
+
const s = context.styles;
|
|
346
|
+
const cols = context.width;
|
|
347
|
+
const rows = context.height;
|
|
348
|
+
const border = renderBorder(cols, s);
|
|
349
|
+
const lines = [];
|
|
350
|
+
const editIndicator = this.state.isDirty ? s.warning('[EDIT*]') : s.primary('[EDIT]');
|
|
351
|
+
lines.push(border);
|
|
352
|
+
lines.push(` ${s.primaryBold('SKILL')}${s.muted(' │ ')}${chalk.bold(this.state.skillName)} ${editIndicator}`);
|
|
353
|
+
lines.push(border);
|
|
354
|
+
const headerLines = 3;
|
|
355
|
+
const footerLines = 3;
|
|
356
|
+
const contentHeight = rows - headerLines - footerLines;
|
|
357
|
+
this.ensureCursorVisible(contentHeight);
|
|
358
|
+
const totalLines = this.state.rawLines.length;
|
|
359
|
+
const lineNumWidth = String(totalLines).length + 1;
|
|
360
|
+
const theme = getCurrentTheme();
|
|
361
|
+
// Pre-compute code block state
|
|
362
|
+
let codeBlockState = { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
|
|
363
|
+
const lineStates = [];
|
|
364
|
+
for (let i = 0; i < totalLines; i++) {
|
|
365
|
+
const line = this.state.rawLines[i];
|
|
366
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)\s*$/);
|
|
367
|
+
if (fenceMatch) {
|
|
368
|
+
const [, fence, lang] = fenceMatch;
|
|
369
|
+
const fenceChar = fence[0];
|
|
370
|
+
const fenceLength = fence.length;
|
|
371
|
+
if (!codeBlockState.inCodeBlock) {
|
|
372
|
+
codeBlockState = { inCodeBlock: true, language: lang || 'plaintext', fenceChar, fenceLength };
|
|
373
|
+
}
|
|
374
|
+
else if (fenceChar === codeBlockState.fenceChar && fenceLength >= codeBlockState.fenceLength) {
|
|
375
|
+
codeBlockState = { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
lineStates[i] = { ...codeBlockState };
|
|
379
|
+
}
|
|
380
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
381
|
+
const lineIndex = this.state.editScrollOffset + i;
|
|
382
|
+
if (lineIndex < totalLines) {
|
|
383
|
+
const lineNum = String(lineIndex + 1).padStart(lineNumWidth);
|
|
384
|
+
const lineContent = this.state.rawLines[lineIndex];
|
|
385
|
+
const isCursorLine = lineIndex === this.state.cursorLine;
|
|
386
|
+
if (isCursorLine) {
|
|
387
|
+
const rendered = this.renderLineWithCursor(lineContent, this.state.cursorColumn, cols - lineNumWidth - 2);
|
|
388
|
+
lines.push(s.muted(`${lineNum}│`) + rendered);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const maxLen = cols - lineNumWidth - 2;
|
|
392
|
+
const truncated = lineContent.length > maxLen;
|
|
393
|
+
const display = truncated ? lineContent.slice(0, maxLen - 1) : lineContent;
|
|
394
|
+
const prevState = lineIndex > 0 ? lineStates[lineIndex - 1] : { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
|
|
395
|
+
const { highlighted } = highlightEditLine(display, prevState, theme);
|
|
396
|
+
lines.push(s.muted(`${lineNum}│`) + highlighted + (truncated ? s.muted('…') : ''));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const lineNum = ' '.repeat(lineNumWidth);
|
|
401
|
+
lines.push(s.muted(`${lineNum}│`) + s.muted('~'));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
lines.push(border);
|
|
405
|
+
if (this.state.showSavePrompt) {
|
|
406
|
+
lines.push(s.warning(' Unsaved changes. Save? ') + s.primary('(y)') + 'es / ' + s.primary('(n)') + 'o / ' + s.primary('(c)') + 'ancel');
|
|
407
|
+
}
|
|
408
|
+
else if (this.state.saveMessage) {
|
|
409
|
+
lines.push(s.success(` ${this.state.saveMessage}`));
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const cursorPos = `Ln ${String(this.state.cursorLine + 1)}, Col ${String(this.state.cursorColumn + 1)}`;
|
|
413
|
+
const mod = this.state.isDirty ? s.warning('[modified]') : '';
|
|
414
|
+
lines.push(s.primaryBold(' ── INSERT ── ') + s.muted(cursorPos) + ' ' + mod + s.muted(' Ctrl+S Save · Esc Exit'));
|
|
415
|
+
}
|
|
416
|
+
lines.push(border);
|
|
417
|
+
return lines;
|
|
418
|
+
}
|
|
419
|
+
renderLineWithCursor(line, cursorCol, maxLen) {
|
|
420
|
+
const theme = getCurrentTheme();
|
|
421
|
+
const cursorStyle = chalk.inverse;
|
|
422
|
+
const effectiveCursorCol = Math.min(cursorCol, line.length);
|
|
423
|
+
let viewStart = 0;
|
|
424
|
+
if (effectiveCursorCol >= maxLen - 5)
|
|
425
|
+
viewStart = effectiveCursorCol - maxLen + 10;
|
|
426
|
+
viewStart = Math.max(0, viewStart);
|
|
427
|
+
const viewEnd = viewStart + maxLen;
|
|
428
|
+
const visibleLine = line.slice(viewStart, viewEnd);
|
|
429
|
+
const cursorPosInView = effectiveCursorCol - viewStart;
|
|
430
|
+
let result = '';
|
|
431
|
+
for (let i = 0; i < visibleLine.length; i++) {
|
|
432
|
+
result += i === cursorPosInView ? cursorStyle(visibleLine[i]) : visibleLine[i];
|
|
433
|
+
}
|
|
434
|
+
if (cursorPosInView >= visibleLine.length)
|
|
435
|
+
result += cursorStyle(' ');
|
|
436
|
+
if (viewStart > 0)
|
|
437
|
+
result = chalk.hex(theme.colors.muted)('…') + result.slice(1);
|
|
438
|
+
if (line.length > viewEnd)
|
|
439
|
+
result = result.slice(0, -1) + chalk.hex(theme.colors.muted)('…');
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
ensureCursorVisible(contentHeight) {
|
|
443
|
+
if (this.state.cursorLine < this.state.editScrollOffset) {
|
|
444
|
+
this.state.editScrollOffset = this.state.cursorLine;
|
|
445
|
+
}
|
|
446
|
+
if (this.state.cursorLine >= this.state.editScrollOffset + contentHeight) {
|
|
447
|
+
this.state.editScrollOffset = this.state.cursorLine - contentHeight + 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ===========================================================================
|
|
451
|
+
// Diff Mode
|
|
452
|
+
// ===========================================================================
|
|
453
|
+
renderDiffMode(context) {
|
|
454
|
+
const s = context.styles;
|
|
455
|
+
const cols = context.width;
|
|
456
|
+
const rows = context.height;
|
|
457
|
+
const border = renderBorder(cols, s);
|
|
458
|
+
const lines = [];
|
|
459
|
+
const diff = this.state.diffLines ?? [];
|
|
460
|
+
lines.push(border);
|
|
461
|
+
const upstream = this.state.forkedFrom?.skill ?? 'unknown';
|
|
462
|
+
lines.push(` ${s.primaryBold('DIFF')}${s.muted(' │ ')}${chalk.bold(this.state.skillName)} ${s.muted('vs')} ${s.secondary(`sdk:${upstream}`)}`);
|
|
463
|
+
lines.push(border);
|
|
464
|
+
const headerUsed = 3;
|
|
465
|
+
const footerLines = 3;
|
|
466
|
+
const contentHeight = rows - headerUsed - footerLines;
|
|
467
|
+
const totalLines = diff.length;
|
|
468
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
469
|
+
const lineIndex = this.state.diffScrollOffset + i;
|
|
470
|
+
if (lineIndex < totalLines) {
|
|
471
|
+
const d = diff[lineIndex];
|
|
472
|
+
const maxLen = cols - 4;
|
|
473
|
+
const content = d.content.length > maxLen ? d.content.slice(0, maxLen - 1) + '…' : d.content;
|
|
474
|
+
if (d.type === 'added') {
|
|
475
|
+
lines.push(` ${s.success('+')} ${s.success(content)}`);
|
|
476
|
+
}
|
|
477
|
+
else if (d.type === 'removed') {
|
|
478
|
+
lines.push(` ${s.warning('-')} ${s.warning(content)}`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
lines.push(` ${s.muted(content)}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
lines.push('');
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const changed = diff.filter((d) => d.type !== 'same').length;
|
|
489
|
+
const canScrollUp = this.state.diffScrollOffset > 0;
|
|
490
|
+
const canScrollDown = this.state.diffScrollOffset + contentHeight < totalLines;
|
|
491
|
+
const arrows = (canScrollUp ? '▲' : ' ') + (canScrollDown ? '▼' : ' ');
|
|
492
|
+
lines.push(border);
|
|
493
|
+
lines.push(s.muted(` ${arrows} ${String(changed)} changed lines · ${String(totalLines)} total · ↑↓/jk Scroll · q/Esc Back`));
|
|
494
|
+
lines.push(border);
|
|
495
|
+
return lines;
|
|
496
|
+
}
|
|
497
|
+
// ===========================================================================
|
|
498
|
+
// Key Handling
|
|
499
|
+
// ===========================================================================
|
|
500
|
+
handleKey(key) {
|
|
501
|
+
const data = key.raw;
|
|
502
|
+
if (this.state.showSavePrompt)
|
|
503
|
+
return this.handleSavePromptKey(data);
|
|
504
|
+
if (this.state.mode === 'edit')
|
|
505
|
+
return this.handleEditModeKey(data);
|
|
506
|
+
if (this.state.mode === 'diff')
|
|
507
|
+
return this.handleDiffModeKey(data);
|
|
508
|
+
return this.handleViewModeKey(data);
|
|
509
|
+
}
|
|
510
|
+
// ===========================================================================
|
|
511
|
+
// View Mode Keys
|
|
512
|
+
// ===========================================================================
|
|
513
|
+
handleViewModeKey(data) {
|
|
514
|
+
const char = getPrintableChar(data);
|
|
515
|
+
const keyStr = data.toString();
|
|
516
|
+
const rows = terminal.getTerminalHeight();
|
|
517
|
+
const contentHeight = rows - 6;
|
|
518
|
+
const totalLines = this.state.contentLines.length;
|
|
519
|
+
const maxScroll = Math.max(0, totalLines - contentHeight);
|
|
520
|
+
if (isCtrlC(data))
|
|
521
|
+
return this.closeWith({ goBack: false, modified: this.state.isDirty });
|
|
522
|
+
if (isClose(data))
|
|
523
|
+
return this.closeWith({ goBack: true, modified: this.state.isDirty });
|
|
524
|
+
// e/i — edit mode
|
|
525
|
+
if (isEditKey(data)) {
|
|
526
|
+
this.state.mode = 'edit';
|
|
527
|
+
this.state.cursorLine = 0;
|
|
528
|
+
this.state.cursorColumn = 0;
|
|
529
|
+
this.state.editScrollOffset = 0;
|
|
530
|
+
this.state.saveMessage = null;
|
|
531
|
+
return this.renderAction();
|
|
532
|
+
}
|
|
533
|
+
// D — diff mode (forked skills only)
|
|
534
|
+
if (char === 'D' && this.state.diffLines) {
|
|
535
|
+
this.state.mode = 'diff';
|
|
536
|
+
this.state.diffScrollOffset = 0;
|
|
537
|
+
return this.renderAction();
|
|
538
|
+
}
|
|
539
|
+
// Scroll
|
|
540
|
+
if (isNavigateUp(data)) {
|
|
541
|
+
if (this.state.scrollOffset > 0)
|
|
542
|
+
this.state.scrollOffset--;
|
|
543
|
+
return this.renderAction();
|
|
544
|
+
}
|
|
545
|
+
if (isNavigateDown(data)) {
|
|
546
|
+
if (this.state.scrollOffset < maxScroll)
|
|
547
|
+
this.state.scrollOffset++;
|
|
548
|
+
return this.renderAction();
|
|
549
|
+
}
|
|
550
|
+
if (keyStr === '\x1b[5~') {
|
|
551
|
+
this.state.scrollOffset = Math.max(0, this.state.scrollOffset - contentHeight);
|
|
552
|
+
return this.renderAction();
|
|
553
|
+
}
|
|
554
|
+
if (keyStr === '\x1b[6~' || isSpace(data)) {
|
|
555
|
+
this.state.scrollOffset = Math.min(maxScroll, this.state.scrollOffset + contentHeight);
|
|
556
|
+
return this.renderAction();
|
|
557
|
+
}
|
|
558
|
+
if (char === 'g') {
|
|
559
|
+
this.state.scrollOffset = 0;
|
|
560
|
+
return this.renderAction();
|
|
561
|
+
}
|
|
562
|
+
if (char === 'G') {
|
|
563
|
+
this.state.scrollOffset = maxScroll;
|
|
564
|
+
return this.renderAction();
|
|
565
|
+
}
|
|
566
|
+
if (isHome(data)) {
|
|
567
|
+
this.state.scrollOffset = 0;
|
|
568
|
+
return this.renderAction();
|
|
569
|
+
}
|
|
570
|
+
if (isEnd(data)) {
|
|
571
|
+
this.state.scrollOffset = maxScroll;
|
|
572
|
+
return this.renderAction();
|
|
573
|
+
}
|
|
574
|
+
return this.noAction();
|
|
575
|
+
}
|
|
576
|
+
// ===========================================================================
|
|
577
|
+
// Diff Mode Keys
|
|
578
|
+
// ===========================================================================
|
|
579
|
+
handleDiffModeKey(data) {
|
|
580
|
+
const char = getPrintableChar(data);
|
|
581
|
+
const rows = terminal.getTerminalHeight();
|
|
582
|
+
const contentHeight = rows - 6;
|
|
583
|
+
const totalLines = this.state.diffLines?.length ?? 0;
|
|
584
|
+
const maxScroll = Math.max(0, totalLines - contentHeight);
|
|
585
|
+
if (isCtrlC(data))
|
|
586
|
+
return this.closeWith({ goBack: false, modified: false });
|
|
587
|
+
if (isClose(data) || isEscape(data)) {
|
|
588
|
+
this.state.mode = 'view';
|
|
589
|
+
return this.renderAction();
|
|
590
|
+
}
|
|
591
|
+
if (isNavigateUp(data)) {
|
|
592
|
+
if (this.state.diffScrollOffset > 0)
|
|
593
|
+
this.state.diffScrollOffset--;
|
|
594
|
+
return this.renderAction();
|
|
595
|
+
}
|
|
596
|
+
if (isNavigateDown(data)) {
|
|
597
|
+
if (this.state.diffScrollOffset < maxScroll)
|
|
598
|
+
this.state.diffScrollOffset++;
|
|
599
|
+
return this.renderAction();
|
|
600
|
+
}
|
|
601
|
+
if (char === 'g') {
|
|
602
|
+
this.state.diffScrollOffset = 0;
|
|
603
|
+
return this.renderAction();
|
|
604
|
+
}
|
|
605
|
+
if (char === 'G') {
|
|
606
|
+
this.state.diffScrollOffset = maxScroll;
|
|
607
|
+
return this.renderAction();
|
|
608
|
+
}
|
|
609
|
+
if (isPageUp(data)) {
|
|
610
|
+
this.state.diffScrollOffset = Math.max(0, this.state.diffScrollOffset - contentHeight);
|
|
611
|
+
return this.renderAction();
|
|
612
|
+
}
|
|
613
|
+
if (isPageDown(data) || isSpace(data)) {
|
|
614
|
+
this.state.diffScrollOffset = Math.min(maxScroll, this.state.diffScrollOffset + contentHeight);
|
|
615
|
+
return this.renderAction();
|
|
616
|
+
}
|
|
617
|
+
return this.noAction();
|
|
618
|
+
}
|
|
619
|
+
// ===========================================================================
|
|
620
|
+
// Edit Mode Keys
|
|
621
|
+
// ===========================================================================
|
|
622
|
+
handleEditModeKey(data) {
|
|
623
|
+
if (this.state.saveMessage)
|
|
624
|
+
this.state.saveMessage = null;
|
|
625
|
+
if (isCtrlC(data))
|
|
626
|
+
return this.closeWith({ goBack: false, modified: this.state.isDirty });
|
|
627
|
+
if (isCtrlS(data)) {
|
|
628
|
+
void this.saveSkill();
|
|
629
|
+
return this.renderAction();
|
|
630
|
+
}
|
|
631
|
+
if (isEscape(data)) {
|
|
632
|
+
if (this.state.isDirty) {
|
|
633
|
+
this.state.showSavePrompt = true;
|
|
634
|
+
return this.renderAction();
|
|
635
|
+
}
|
|
636
|
+
return this.exitEditMode();
|
|
637
|
+
}
|
|
638
|
+
// Navigation
|
|
639
|
+
if (isUpArrow(data))
|
|
640
|
+
return this.moveCursorUp();
|
|
641
|
+
if (isDownArrow(data))
|
|
642
|
+
return this.moveCursorDown();
|
|
643
|
+
if (isLeftArrow(data))
|
|
644
|
+
return this.moveCursorLeft();
|
|
645
|
+
if (isRightArrow(data))
|
|
646
|
+
return this.moveCursorRight();
|
|
647
|
+
if (isHome(data)) {
|
|
648
|
+
this.state.cursorColumn = 0;
|
|
649
|
+
return this.renderAction();
|
|
650
|
+
}
|
|
651
|
+
if (isEnd(data)) {
|
|
652
|
+
this.state.cursorColumn = this.getCurrentLine().length;
|
|
653
|
+
return this.renderAction();
|
|
654
|
+
}
|
|
655
|
+
if (isPageUp(data)) {
|
|
656
|
+
const pageSize = terminal.getTerminalHeight() - 6;
|
|
657
|
+
this.state.cursorLine = Math.max(0, this.state.cursorLine - pageSize);
|
|
658
|
+
this.clampCursorColumn();
|
|
659
|
+
return this.renderAction();
|
|
660
|
+
}
|
|
661
|
+
if (isPageDown(data)) {
|
|
662
|
+
const pageSize = terminal.getTerminalHeight() - 6;
|
|
663
|
+
this.state.cursorLine = Math.min(this.state.rawLines.length - 1, this.state.cursorLine + pageSize);
|
|
664
|
+
this.clampCursorColumn();
|
|
665
|
+
return this.renderAction();
|
|
666
|
+
}
|
|
667
|
+
// Editing
|
|
668
|
+
if (isBackspace(data))
|
|
669
|
+
return this.handleBackspace();
|
|
670
|
+
if (isDelete(data))
|
|
671
|
+
return this.handleDeleteKey();
|
|
672
|
+
if (isEnter(data))
|
|
673
|
+
return this.handleEnter();
|
|
674
|
+
if (isTab(data))
|
|
675
|
+
return this.insertText(' '.repeat(TAB_SIZE));
|
|
676
|
+
const char = getPrintableChar(data);
|
|
677
|
+
if (char)
|
|
678
|
+
return this.insertText(char);
|
|
679
|
+
return this.noAction();
|
|
680
|
+
}
|
|
681
|
+
// ===========================================================================
|
|
682
|
+
// Save Prompt Keys
|
|
683
|
+
// ===========================================================================
|
|
684
|
+
handleSavePromptKey(data) {
|
|
685
|
+
const char = getPrintableChar(data);
|
|
686
|
+
if (char === 'y' || char === 'Y') {
|
|
687
|
+
void this.saveSkill();
|
|
688
|
+
this.state.showSavePrompt = false;
|
|
689
|
+
return this.exitEditMode();
|
|
690
|
+
}
|
|
691
|
+
if (char === 'n' || char === 'N') {
|
|
692
|
+
this.state.rawLines = this.state.originalContent.split('\n');
|
|
693
|
+
this.state.isDirty = false;
|
|
694
|
+
this.state.showSavePrompt = false;
|
|
695
|
+
return this.exitEditMode();
|
|
696
|
+
}
|
|
697
|
+
if (char === 'c' || char === 'C' || isEscape(data)) {
|
|
698
|
+
this.state.showSavePrompt = false;
|
|
699
|
+
return this.renderAction();
|
|
700
|
+
}
|
|
701
|
+
if (isCtrlC(data))
|
|
702
|
+
return this.closeWith({ goBack: false, modified: false });
|
|
703
|
+
return this.noAction();
|
|
704
|
+
}
|
|
705
|
+
// ===========================================================================
|
|
706
|
+
// Cursor Movement
|
|
707
|
+
// ===========================================================================
|
|
708
|
+
moveCursorUp() {
|
|
709
|
+
if (this.state.cursorLine > 0) {
|
|
710
|
+
this.state.cursorLine--;
|
|
711
|
+
this.clampCursorColumn();
|
|
712
|
+
}
|
|
713
|
+
return this.renderAction();
|
|
714
|
+
}
|
|
715
|
+
moveCursorDown() {
|
|
716
|
+
if (this.state.cursorLine < this.state.rawLines.length - 1) {
|
|
717
|
+
this.state.cursorLine++;
|
|
718
|
+
this.clampCursorColumn();
|
|
719
|
+
}
|
|
720
|
+
return this.renderAction();
|
|
721
|
+
}
|
|
722
|
+
moveCursorLeft() {
|
|
723
|
+
if (this.state.cursorColumn > 0) {
|
|
724
|
+
this.state.cursorColumn--;
|
|
725
|
+
}
|
|
726
|
+
else if (this.state.cursorLine > 0) {
|
|
727
|
+
this.state.cursorLine--;
|
|
728
|
+
this.state.cursorColumn = this.getCurrentLine().length;
|
|
729
|
+
}
|
|
730
|
+
return this.renderAction();
|
|
731
|
+
}
|
|
732
|
+
moveCursorRight() {
|
|
733
|
+
const lineLen = this.getCurrentLine().length;
|
|
734
|
+
if (this.state.cursorColumn < lineLen) {
|
|
735
|
+
this.state.cursorColumn++;
|
|
736
|
+
}
|
|
737
|
+
else if (this.state.cursorLine < this.state.rawLines.length - 1) {
|
|
738
|
+
this.state.cursorLine++;
|
|
739
|
+
this.state.cursorColumn = 0;
|
|
740
|
+
}
|
|
741
|
+
return this.renderAction();
|
|
742
|
+
}
|
|
743
|
+
clampCursorColumn() {
|
|
744
|
+
this.state.cursorColumn = Math.min(this.state.cursorColumn, this.getCurrentLine().length);
|
|
745
|
+
}
|
|
746
|
+
getCurrentLine() {
|
|
747
|
+
return this.state.rawLines[this.state.cursorLine] || '';
|
|
748
|
+
}
|
|
749
|
+
// ===========================================================================
|
|
750
|
+
// Text Editing
|
|
751
|
+
// ===========================================================================
|
|
752
|
+
insertText(text) {
|
|
753
|
+
const line = this.getCurrentLine();
|
|
754
|
+
this.state.rawLines[this.state.cursorLine] = line.slice(0, this.state.cursorColumn) + text + line.slice(this.state.cursorColumn);
|
|
755
|
+
this.state.cursorColumn += text.length;
|
|
756
|
+
this.state.isDirty = true;
|
|
757
|
+
return this.renderAction();
|
|
758
|
+
}
|
|
759
|
+
handleBackspace() {
|
|
760
|
+
if (this.state.cursorColumn > 0) {
|
|
761
|
+
const line = this.getCurrentLine();
|
|
762
|
+
this.state.rawLines[this.state.cursorLine] = line.slice(0, this.state.cursorColumn - 1) + line.slice(this.state.cursorColumn);
|
|
763
|
+
this.state.cursorColumn--;
|
|
764
|
+
this.state.isDirty = true;
|
|
765
|
+
}
|
|
766
|
+
else if (this.state.cursorLine > 0) {
|
|
767
|
+
const currentLine = this.getCurrentLine();
|
|
768
|
+
const prevLine = this.state.rawLines[this.state.cursorLine - 1];
|
|
769
|
+
this.state.rawLines[this.state.cursorLine - 1] = prevLine + currentLine;
|
|
770
|
+
this.state.rawLines.splice(this.state.cursorLine, 1);
|
|
771
|
+
this.state.cursorLine--;
|
|
772
|
+
this.state.cursorColumn = prevLine.length;
|
|
773
|
+
this.state.isDirty = true;
|
|
774
|
+
}
|
|
775
|
+
return this.renderAction();
|
|
776
|
+
}
|
|
777
|
+
handleDeleteKey() {
|
|
778
|
+
const line = this.getCurrentLine();
|
|
779
|
+
if (this.state.cursorColumn < line.length) {
|
|
780
|
+
this.state.rawLines[this.state.cursorLine] = line.slice(0, this.state.cursorColumn) + line.slice(this.state.cursorColumn + 1);
|
|
781
|
+
this.state.isDirty = true;
|
|
782
|
+
}
|
|
783
|
+
else if (this.state.cursorLine < this.state.rawLines.length - 1) {
|
|
784
|
+
const nextLine = this.state.rawLines[this.state.cursorLine + 1];
|
|
785
|
+
this.state.rawLines[this.state.cursorLine] = line + nextLine;
|
|
786
|
+
this.state.rawLines.splice(this.state.cursorLine + 1, 1);
|
|
787
|
+
this.state.isDirty = true;
|
|
788
|
+
}
|
|
789
|
+
return this.renderAction();
|
|
790
|
+
}
|
|
791
|
+
handleEnter() {
|
|
792
|
+
const line = this.getCurrentLine();
|
|
793
|
+
this.state.rawLines[this.state.cursorLine] = line.slice(0, this.state.cursorColumn);
|
|
794
|
+
this.state.rawLines.splice(this.state.cursorLine + 1, 0, line.slice(this.state.cursorColumn));
|
|
795
|
+
this.state.cursorLine++;
|
|
796
|
+
this.state.cursorColumn = 0;
|
|
797
|
+
this.state.isDirty = true;
|
|
798
|
+
return this.renderAction();
|
|
799
|
+
}
|
|
800
|
+
// ===========================================================================
|
|
801
|
+
// Save + Validate
|
|
802
|
+
// ===========================================================================
|
|
803
|
+
async saveSkill() {
|
|
804
|
+
const content = this.state.rawLines.join('\n');
|
|
805
|
+
try {
|
|
806
|
+
await fsAsync.writeFile(this.filePath, content);
|
|
807
|
+
this.state.originalContent = content;
|
|
808
|
+
this.state.isDirty = false;
|
|
809
|
+
// Re-validate
|
|
810
|
+
const parsed = parseSkillMarkdown(content);
|
|
811
|
+
const warnings = parsed
|
|
812
|
+
? validateSkillQuality(parsed, this.state.skillName).map((i) => i.message)
|
|
813
|
+
: ['Invalid frontmatter — required fields: name, description.'];
|
|
814
|
+
this.state.validationWarnings = warnings;
|
|
815
|
+
// Re-compute diff if forked
|
|
816
|
+
if (this.state.forkedFrom && parsed?.prompt) {
|
|
817
|
+
this.state.diffLines = diffForkVsUpstream(parsed.prompt, this.state.forkedFrom.skill);
|
|
818
|
+
}
|
|
819
|
+
if (warnings.length > 0) {
|
|
820
|
+
this.state.saveMessage = `⚠ Saved with ${String(warnings.length)} warning${warnings.length > 1 ? 's' : ''}: ${warnings.join('; ')}`;
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
this.state.saveMessage = '✓ Saved';
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
this.state.saveMessage = `Error: ${error.message}`;
|
|
828
|
+
}
|
|
829
|
+
this.requestRender();
|
|
830
|
+
}
|
|
831
|
+
exitEditMode() {
|
|
832
|
+
this.state.mode = 'view';
|
|
833
|
+
const content = this.state.rawLines.join('\n');
|
|
834
|
+
const bodyContent = extractBody(content);
|
|
835
|
+
const termWidth = terminal.getTerminalWidth();
|
|
836
|
+
this.state.contentLines = renderMarkdownSync(bodyContent, termWidth);
|
|
837
|
+
this.state.scrollOffset = 0;
|
|
838
|
+
return this.renderAction();
|
|
839
|
+
}
|
|
840
|
+
// ===========================================================================
|
|
841
|
+
// Helpers
|
|
842
|
+
// ===========================================================================
|
|
843
|
+
closeWith(result) {
|
|
844
|
+
return { type: 'close', result };
|
|
845
|
+
}
|
|
846
|
+
renderAction() {
|
|
847
|
+
return { type: 'render' };
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Extract the body content from a SKILL.md (skip frontmatter).
|
|
852
|
+
*/
|
|
853
|
+
function extractBody(content) {
|
|
854
|
+
if (!content.startsWith('---'))
|
|
855
|
+
return content;
|
|
856
|
+
const lines = content.split('\n');
|
|
857
|
+
for (let i = 1; i < lines.length; i++) {
|
|
858
|
+
if (lines[i] === '---') {
|
|
859
|
+
return lines.slice(i + 1).join('\n').trim();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return content;
|
|
863
|
+
}
|