Skip to content

Commit d814d5d

Browse files
thegreatalxxclaude
andcommitted
feat: 20 new features — themes, plugins, TF-IDF search, vision, history + more
Features added: 1. Persistent input history (~/.localcode/history.json, 200 entries) 2. Spinner animation — braille spinner during streaming/tool calls 3. Word wrapping — all message text wraps at terminal width 4. Theme system — dark/nord/monokai/light (/theme command) 5. Template system — /template add/use/list/delete (~/.localcode/templates.json) 6. Alias system — /alias name command (~/.localcode/aliases.json) 7. /explain — stream explanation of a file or last code snippet 8. /test — auto-detect jest/vitest/pytest/cargo/go test and run it 9. /share — export conversation as self-contained HTML page 10. Session history browser — /history [n], archives in ~/.localcode/sessions/ 11. /git panel — status/log/stash/branch/passthrough 12. /watch — fs.watch file, re-run last message on change (/watch stop) 13. Auto-context on startup — inject git log+status if no .nyx.md found 14. Multi-file diff summary — shows +/- counts after agent modifies files 15. Collapsible tool output — truncates long output with [+N chars] indicator 16. Syntax highlighting — keywords/strings/numbers/comments per language 17. Plugin system — load ~/.localcode/plugins/*.js at startup (/plugins) 18. /image — vision input (base64), works with Claude/OpenAI/Ollama llava 19. Streaming token counter — live +N▌ in header during generation 20. TF-IDF semantic search — /index builds index, /search uses it New files: src/plugins/loader.ts, src/search/tfidf.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d9c5dfa commit d814d5d

9 files changed

Lines changed: 1624 additions & 263 deletions

File tree

src/core/types.ts

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface ProviderConfig {
1717
export interface Message {
1818
role: 'user' | 'assistant' | 'system';
1919
content: string;
20+
images?: Array<{ base64: string; mimeType: string }>;
2021
}
2122

2223
export interface ToolCall {
@@ -51,6 +52,28 @@ export interface Persona {
5152
prompt: string;
5253
}
5354

55+
// ── Theme system ─────────────────────────────────────────────────────────────
56+
57+
export type ThemeName = 'dark' | 'nord' | 'monokai' | 'light';
58+
59+
export interface Theme {
60+
name: ThemeName;
61+
primary: string; // user prompt color
62+
accent: string; // assistant icon color
63+
tool: string; // tool message color
64+
system: string; // system message color
65+
error: string; // error color
66+
border: string; // input box border color
67+
header: string; // header art color
68+
}
69+
70+
export const THEMES: Record<ThemeName, Theme> = {
71+
dark: { name: 'dark', primary: 'yellowBright', accent: 'white', tool: 'cyan', system: 'gray', error: 'red', border: 'yellowBright', header: 'white' },
72+
nord: { name: 'nord', primary: 'blueBright', accent: 'cyanBright', tool: 'cyan', system: 'gray', error: 'red', border: 'blueBright', header: 'cyanBright' },
73+
monokai: { name: 'monokai', primary: 'greenBright', accent: 'magentaBright', tool: 'yellowBright', system: 'gray', error: 'red', border: 'greenBright', header: 'magentaBright' },
74+
light: { name: 'light', primary: 'blue', accent: 'black', tool: 'cyan', system: 'gray', error: 'red', border: 'blue', header: 'blue' },
75+
};
76+
5477
export interface SessionState {
5578
provider: Provider;
5679
model: string;
@@ -67,6 +90,7 @@ export interface SessionState {
6790
maxSteps: number; // max agent tool-call iterations per response
6891
sessionCost: number; // estimated USD cost this session
6992
lastAssistantMessage: string; // for /retry and /copy
93+
theme: ThemeName; // UI color theme
7094
}
7195

7296
export const DEFAULT_SYSTEM_PROMPT = `You are Nyx, an AI coding assistant built into LocalCode — a terminal tool made by TheAlxLabs.
@@ -270,6 +294,15 @@ export const SLASH_COMMANDS: SlashCommand[] = [
270294
usage: '/steps 40',
271295
category: 'session',
272296
},
297+
{
298+
name: 'theme',
299+
trigger: '/theme',
300+
icon: '◑',
301+
description: 'Switch color theme',
302+
detail: 'List available themes or switch to one: dark, nord, monokai, light.',
303+
usage: '/theme | /theme nord | /theme monokai',
304+
category: 'session',
305+
},
273306
// ── System & Personas ─────────────────────────────────────────────────────
274307
{
275308
name: 'sys',
@@ -333,6 +366,24 @@ export const SLASH_COMMANDS: SlashCommand[] = [
333366
usage: '/open src/app.ts',
334367
category: 'context',
335368
},
369+
{
370+
name: 'template',
371+
trigger: '/template',
372+
icon: '⊞',
373+
description: 'Manage prompt templates',
374+
detail: 'List, add, use, or delete prompt templates. Usage: /template list | /template add <name> <prompt> | /template use <name> | /template delete <name>',
375+
usage: '/template | /template add mytemplate Fix all TypeScript errors | /template use mytemplate',
376+
category: 'context',
377+
},
378+
{
379+
name: 'alias',
380+
trigger: '/alias',
381+
icon: '⇒',
382+
description: 'Manage command aliases',
383+
detail: 'Create shortcuts for commands. Usage: /alias | /alias <name> <command> | /alias delete <name>',
384+
usage: '/alias | /alias review /review | /alias delete review',
385+
category: 'session',
386+
},
336387
{
337388
name: 'models',
338389
trigger: '/models',
@@ -387,6 +438,33 @@ export const SLASH_COMMANDS: SlashCommand[] = [
387438
usage: '/commit',
388439
category: 'git',
389440
},
441+
{
442+
name: 'git',
443+
trigger: '/git',
444+
icon: '⎇',
445+
description: 'Run git commands and show results',
446+
detail: 'Quick git panel: status, log, stash, branch, or any git command. Usage: /git | /git log | /git stash | /git branch',
447+
usage: '/git | /git log | /git stash | /git branch | /git diff HEAD',
448+
category: 'git',
449+
},
450+
{
451+
name: 'history',
452+
trigger: '/history',
453+
icon: '◷',
454+
description: 'Browse and restore session history',
455+
detail: 'List recent sessions or restore one by index.',
456+
usage: '/history | /history 3',
457+
category: 'session',
458+
},
459+
{
460+
name: 'share',
461+
trigger: '/share',
462+
icon: '↗',
463+
description: 'Export conversation as self-contained HTML',
464+
detail: 'Generates a beautiful HTML file of the conversation with syntax highlighting.',
465+
usage: '/share',
466+
category: 'session',
467+
},
390468
// ── Tools ─────────────────────────────────────────────────────────────────
391469
{
392470
name: 'init',
@@ -432,6 +510,60 @@ export const SLASH_COMMANDS: SlashCommand[] = [
432510
usage: '/mcp list | /mcp add <n> stdio <cmd> | /mcp tools',
433511
category: 'tools',
434512
},
513+
{
514+
name: 'watch',
515+
trigger: '/watch',
516+
icon: '◉',
517+
description: 'Watch a file for changes and re-run last message',
518+
detail: 'Automatically re-sends your last message when a file changes. Usage: /watch <file> | /watch stop',
519+
usage: '/watch src/app.ts | /watch stop | /watch',
520+
category: 'tools',
521+
},
522+
{
523+
name: 'explain',
524+
trigger: '/explain',
525+
icon: '◈',
526+
description: 'Explain code — a file or the last snippet',
527+
detail: 'Pass a file path to explain that file, or use with no args to explain the last code snippet.',
528+
usage: '/explain | /explain src/app.ts',
529+
category: 'tools',
530+
},
531+
{
532+
name: 'test',
533+
trigger: '/test',
534+
icon: '✚',
535+
description: 'Run tests using the detected test runner',
536+
detail: 'Detects jest/vitest/pytest/cargo/go and runs tests. Offers to fix failures.',
537+
usage: '/test',
538+
category: 'tools',
539+
},
540+
{
541+
name: 'image',
542+
trigger: '/image',
543+
icon: '⊞',
544+
description: 'Load an image file for vision analysis',
545+
detail: 'Read an image file and add it to the conversation for vision-capable models.',
546+
usage: '/image screenshot.png',
547+
category: 'context',
548+
},
549+
{
550+
name: 'index',
551+
trigger: '/index',
552+
icon: '⌖',
553+
description: 'Build TF-IDF search index for working directory',
554+
detail: 'Indexes all text files for semantic search. Use /search after indexing for better results.',
555+
usage: '/index',
556+
category: 'tools',
557+
},
558+
{
559+
name: 'plugins',
560+
trigger: '/plugins',
561+
icon: '⬡',
562+
description: 'List loaded plugins',
563+
detail: 'Shows all plugins loaded from ~/.localcode/plugins/',
564+
usage: '/plugins',
565+
category: 'tools',
566+
},
435567
// ── Providers ─────────────────────────────────────────────────────────────
436568
{
437569
name: 'provider',
@@ -483,8 +615,8 @@ export const SLASH_COMMANDS: SlashCommand[] = [
483615
name: 'search',
484616
trigger: '/search',
485617
icon: '⌖',
486-
description: 'Search file contents (grep)',
487-
detail: 'Directly search for a pattern across all files in the working directory.',
618+
description: 'Search file contents (grep or TF-IDF if indexed)',
619+
detail: 'Search for a pattern. If /index has been run, uses TF-IDF scoring for semantic results.',
488620
usage: '/search TODO | /search "function render"',
489621
category: 'tools',
490622
},

src/plugins/loader.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// src/plugins/loader.ts
2+
// Dynamic plugin loader for LocalCode
3+
4+
import * as fs from 'fs';
5+
import * as path from 'path';
6+
import * as os from 'os';
7+
8+
export interface PluginContext {
9+
workingDir: string;
10+
sysMsg: (text: string, isError?: boolean) => void;
11+
addDisplay: (msg: { role: string; content: string; isError?: boolean }) => string;
12+
}
13+
14+
export interface LocalCodePlugin {
15+
name: string;
16+
trigger: string; // e.g. '/myplugin'
17+
description: string;
18+
execute: (args: string, context: PluginContext) => Promise<void>;
19+
}
20+
21+
const PLUGINS_DIR = path.join(os.homedir(), '.localcode', 'plugins');
22+
23+
export async function loadPlugins(): Promise<LocalCodePlugin[]> {
24+
const plugins: LocalCodePlugin[] = [];
25+
26+
try {
27+
if (!fs.existsSync(PLUGINS_DIR)) {
28+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
29+
return plugins;
30+
}
31+
32+
const files = fs.readdirSync(PLUGINS_DIR).filter((f) => f.endsWith('.js'));
33+
34+
for (const file of files) {
35+
const filePath = path.join(PLUGINS_DIR, file);
36+
try {
37+
// Dynamic import using file URL for ESM compatibility
38+
const fileUrl = new URL(`file://${filePath}`);
39+
const mod = await import(fileUrl.href) as { default?: LocalCodePlugin };
40+
const plugin = mod.default;
41+
42+
if (
43+
plugin &&
44+
typeof plugin.name === 'string' &&
45+
typeof plugin.trigger === 'string' &&
46+
typeof plugin.description === 'string' &&
47+
typeof plugin.execute === 'function'
48+
) {
49+
plugins.push(plugin);
50+
}
51+
} catch {
52+
// Skip invalid plugins silently
53+
}
54+
}
55+
} catch {
56+
// If we can't read plugins dir, just return empty
57+
}
58+
59+
return plugins;
60+
}

src/providers/client.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,16 @@ async function runOllamaAgent(
161161
systemPrompt?: string,
162162
signal?: AbortSignal,
163163
): Promise<void> {
164-
const history: Array<{ role: string; content: string }> = [];
164+
type OllamaMsg = { role: string; content: string; images?: string[] };
165+
const history: OllamaMsg[] = [];
165166
if (systemPrompt) history.push({ role: 'system', content: systemPrompt });
166-
for (const m of messages) history.push({ role: m.role, content: m.content });
167+
for (const m of messages) {
168+
const entry: OllamaMsg = { role: m.role, content: m.content };
169+
if (m.images && m.images.length > 0) {
170+
entry.images = m.images.map((img) => img.base64);
171+
}
172+
history.push(entry);
173+
}
167174

168175
const tools = agentCfg.tools.map((t) => ({
169176
type: 'function',
@@ -257,10 +264,21 @@ async function runClaudeAgent(
257264
}));
258265

259266
type AnthropicMsg = { role: 'user' | 'assistant'; content: string | unknown[] };
260-
const history: AnthropicMsg[] = messages.map((m) => ({
261-
role: m.role === 'assistant' ? 'assistant' : 'user',
262-
content: m.content,
263-
}));
267+
const history: AnthropicMsg[] = messages.map((m) => {
268+
if (m.images && m.images.length > 0) {
269+
// Build content array with image blocks + text
270+
const contentArr: unknown[] = m.images.map((img) => ({
271+
type: 'image',
272+
source: { type: 'base64', media_type: img.mimeType, data: img.base64 },
273+
}));
274+
contentArr.push({ type: 'text', text: m.content });
275+
return { role: m.role === 'assistant' ? 'assistant' : 'user', content: contentArr };
276+
}
277+
return {
278+
role: m.role === 'assistant' ? 'assistant' : 'user',
279+
content: m.content,
280+
};
281+
});
264282

265283
for (let step = 0; step < agentCfg.maxSteps; step++) {
266284
if (signal?.aborted) { await onChunk({ type: 'error', error: 'Cancelled.' }); return; }
@@ -396,10 +414,22 @@ async function runOpenAIAgent(
396414
function: { name: t.name, description: t.description, parameters: t.parameters },
397415
}));
398416

399-
type OAIMsg = { role: string; content: string | null; tool_calls?: unknown[]; tool_call_id?: string };
417+
type OAIMsg = { role: string; content: string | unknown[] | null; tool_calls?: unknown[]; tool_call_id?: string };
400418
const history: OAIMsg[] = [];
401419
if (systemPrompt) history.push({ role: 'system', content: systemPrompt });
402-
for (const m of messages) history.push({ role: m.role, content: m.content });
420+
for (const m of messages) {
421+
if (m.images && m.images.length > 0) {
422+
// Build content array with image_url blocks + text
423+
const contentArr: unknown[] = m.images.map((img) => ({
424+
type: 'image_url',
425+
image_url: { url: `data:${img.mimeType};base64,${img.base64}` },
426+
}));
427+
contentArr.push({ type: 'text', text: m.content });
428+
history.push({ role: m.role, content: contentArr });
429+
} else {
430+
history.push({ role: m.role, content: m.content });
431+
}
432+
}
403433

404434
for (let step = 0; step < agentCfg.maxSteps; step++) {
405435
if (signal?.aborted) { await onChunk({ type: 'error', error: 'Cancelled.' }); return; }

0 commit comments

Comments
 (0)