cc-sidebar 0.1.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/LICENSE +21 -0
- package/README.md +128 -0
- package/commands/sidebar.md +62 -0
- package/package.json +40 -0
- package/skills/sidebar-awareness/SKILL.md +69 -0
- package/src/cli.ts +166 -0
- package/src/components/RawSidebar.tsx +1155 -0
- package/src/components/Sidebar.tsx +542 -0
- package/src/ipc/client.ts +96 -0
- package/src/ipc/server.ts +92 -0
- package/src/minimal-test.ts +45 -0
- package/src/node-test.mjs +50 -0
- package/src/persistence/store.ts +346 -0
- package/src/standalone-input.ts +101 -0
- package/src/sync-todos.ts +157 -0
- package/src/terminal/iterm.ts +187 -0
- package/src/terminal/prompt.ts +30 -0
- package/src/terminal/tmux.ts +246 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC Server for sidebar communication
|
|
3
|
+
* Uses Unix domain sockets for real-time updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { unlinkSync, existsSync } from "fs";
|
|
7
|
+
|
|
8
|
+
export interface IPCMessage {
|
|
9
|
+
type: string;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IPCServerOptions {
|
|
14
|
+
socketPath: string;
|
|
15
|
+
onMessage?: (message: IPCMessage) => void;
|
|
16
|
+
onConnect?: () => void;
|
|
17
|
+
onDisconnect?: () => void;
|
|
18
|
+
onError?: (error: Error) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IPCServer {
|
|
22
|
+
broadcast: (message: IPCMessage) => void;
|
|
23
|
+
close: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createIPCServer(options: IPCServerOptions): IPCServer {
|
|
27
|
+
const { socketPath, onMessage, onConnect, onDisconnect, onError } = options;
|
|
28
|
+
|
|
29
|
+
// Clean up existing socket file
|
|
30
|
+
if (existsSync(socketPath)) {
|
|
31
|
+
try {
|
|
32
|
+
unlinkSync(socketPath);
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore cleanup errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const clients = new Set<ReturnType<typeof Bun.listen>["data"]>();
|
|
39
|
+
|
|
40
|
+
const server = Bun.listen({
|
|
41
|
+
unix: socketPath,
|
|
42
|
+
socket: {
|
|
43
|
+
open(socket) {
|
|
44
|
+
clients.add(socket);
|
|
45
|
+
onConnect?.();
|
|
46
|
+
},
|
|
47
|
+
close(socket) {
|
|
48
|
+
clients.delete(socket);
|
|
49
|
+
onDisconnect?.();
|
|
50
|
+
},
|
|
51
|
+
data(socket, data) {
|
|
52
|
+
try {
|
|
53
|
+
const text = Buffer.from(data).toString("utf-8");
|
|
54
|
+
// Handle line-delimited JSON
|
|
55
|
+
const lines = text.split("\n").filter((line) => line.trim());
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const message = JSON.parse(line) as IPCMessage;
|
|
58
|
+
onMessage?.(message);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
error(socket, error) {
|
|
65
|
+
onError?.(error);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
broadcast(message: IPCMessage) {
|
|
72
|
+
const data = JSON.stringify(message) + "\n";
|
|
73
|
+
for (const client of clients) {
|
|
74
|
+
try {
|
|
75
|
+
(client as { write: (data: string) => void }).write(data);
|
|
76
|
+
} catch {
|
|
77
|
+
// Client may have disconnected
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
close() {
|
|
82
|
+
server.stop();
|
|
83
|
+
if (existsSync(socketPath)) {
|
|
84
|
+
try {
|
|
85
|
+
unlinkSync(socketPath);
|
|
86
|
+
} catch {
|
|
87
|
+
// Ignore cleanup errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Minimal terminal input test - absolutely no rendering during input
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Hide cursor and clear screen once
|
|
7
|
+
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
|
|
8
|
+
process.stdout.write('Type something (press Enter to echo, Escape to quit):\n> ');
|
|
9
|
+
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
10
|
+
|
|
11
|
+
let buffer = '';
|
|
12
|
+
|
|
13
|
+
process.stdin.setRawMode(true);
|
|
14
|
+
process.stdin.resume();
|
|
15
|
+
process.stdin.setEncoding('utf8');
|
|
16
|
+
|
|
17
|
+
process.stdin.on('data', (key: string) => {
|
|
18
|
+
// Escape - quit
|
|
19
|
+
if (key === '\x1b') {
|
|
20
|
+
process.stdout.write('\n\x1b[?25h\x1b[0m');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Enter - echo and reset
|
|
25
|
+
if (key === '\r' || key === '\n') {
|
|
26
|
+
process.stdout.write(`\nYou typed: ${buffer}\n> `);
|
|
27
|
+
buffer = '';
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Backspace
|
|
32
|
+
if (key === '\x7f') {
|
|
33
|
+
if (buffer.length > 0) {
|
|
34
|
+
buffer = buffer.slice(0, -1);
|
|
35
|
+
process.stdout.write('\b \b');
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Regular character - just echo it
|
|
41
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
42
|
+
buffer += key;
|
|
43
|
+
process.stdout.write(key);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Node.js minimal terminal input test
|
|
4
|
+
* Testing if the flicker is Bun-specific
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
|
|
9
|
+
// Clear screen and show prompt
|
|
10
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
11
|
+
process.stdout.write('Node.js test - Type something (Enter to echo, Escape to quit):\n> ');
|
|
12
|
+
|
|
13
|
+
readline.emitKeypressEvents(process.stdin);
|
|
14
|
+
if (process.stdin.isTTY) {
|
|
15
|
+
process.stdin.setRawMode(true);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let buffer = '';
|
|
19
|
+
|
|
20
|
+
process.stdin.on('keypress', (str, key) => {
|
|
21
|
+
// Escape or Ctrl+C - quit
|
|
22
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
23
|
+
process.stdout.write('\n');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Enter - echo and reset
|
|
28
|
+
if (key.name === 'return') {
|
|
29
|
+
process.stdout.write(`\nYou typed: ${buffer}\n> `);
|
|
30
|
+
buffer = '';
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Backspace
|
|
35
|
+
if (key.name === 'backspace') {
|
|
36
|
+
if (buffer.length > 0) {
|
|
37
|
+
buffer = buffer.slice(0, -1);
|
|
38
|
+
process.stdout.write('\b \b');
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Regular character
|
|
44
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
|
|
45
|
+
buffer += str;
|
|
46
|
+
process.stdout.write(str);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
process.stdin.resume();
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for sidebar data
|
|
3
|
+
* Stores tasks (queue), active task, and history per-project in ~/.claude-sidebar/projects/<hash>/
|
|
4
|
+
* Global data (statusline, socket) stored in ~/.claude-sidebar/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync } from "fs";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
|
|
12
|
+
const SIDEBAR_DIR = join(homedir(), ".claude-sidebar");
|
|
13
|
+
|
|
14
|
+
// Get a short hash of the working directory for project isolation
|
|
15
|
+
function getProjectHash(): string {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 12);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get the project-specific directory
|
|
21
|
+
function getProjectDir(): string {
|
|
22
|
+
return join(SIDEBAR_DIR, "projects", getProjectHash());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Store a mapping of hash -> path for easier debugging
|
|
26
|
+
function updateProjectMapping(): void {
|
|
27
|
+
const mappingPath = join(SIDEBAR_DIR, "projects", "mapping.json");
|
|
28
|
+
let mapping: Record<string, string> = {};
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(mappingPath)) {
|
|
31
|
+
mapping = JSON.parse(readFileSync(mappingPath, "utf-8"));
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
mapping[getProjectHash()] = process.cwd();
|
|
35
|
+
ensureDir();
|
|
36
|
+
mkdirSync(join(SIDEBAR_DIR, "projects"), { recursive: true });
|
|
37
|
+
writeFileSync(mappingPath, JSON.stringify(mapping, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Task {
|
|
41
|
+
id: string;
|
|
42
|
+
content: string;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
clarified?: boolean; // Was this task clarified via interview/brain-dump?
|
|
45
|
+
priority?: number; // Sort order (lower = higher priority, 1 = most important)
|
|
46
|
+
recommended?: boolean; // Claude's top picks (shown with star)
|
|
47
|
+
planPath?: string; // Filename of associated Atomic Plan
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ActiveTask {
|
|
51
|
+
id: string;
|
|
52
|
+
content: string;
|
|
53
|
+
sentAt: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DoneTask {
|
|
57
|
+
id: string;
|
|
58
|
+
content: string;
|
|
59
|
+
completedAt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface StatuslineData {
|
|
63
|
+
contextPercent: number;
|
|
64
|
+
contextTokens: number;
|
|
65
|
+
contextSize: number;
|
|
66
|
+
costUsd: number;
|
|
67
|
+
durationMin: number;
|
|
68
|
+
model: string;
|
|
69
|
+
branch: string;
|
|
70
|
+
repo: string;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ClaudeConfig {
|
|
75
|
+
enabledPlugins: string[];
|
|
76
|
+
mcpServers: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ClaudeTodo {
|
|
80
|
+
content: string;
|
|
81
|
+
status: "pending" | "in_progress" | "completed";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ClaudeTodosData {
|
|
85
|
+
todos: ClaudeTodo[];
|
|
86
|
+
updatedAt: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get the data directory path
|
|
90
|
+
export function getDataDir(): string {
|
|
91
|
+
return SIDEBAR_DIR;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ensure the sidebar directory exists
|
|
95
|
+
export function ensureDir(): void {
|
|
96
|
+
if (!existsSync(SIDEBAR_DIR)) {
|
|
97
|
+
mkdirSync(SIDEBAR_DIR, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Generic read/write helpers for GLOBAL data
|
|
102
|
+
function readJson<T>(filename: string, defaultValue: T): T {
|
|
103
|
+
const filepath = join(SIDEBAR_DIR, filename);
|
|
104
|
+
try {
|
|
105
|
+
if (existsSync(filepath)) {
|
|
106
|
+
return JSON.parse(readFileSync(filepath, "utf-8"));
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Return default on error
|
|
110
|
+
}
|
|
111
|
+
return defaultValue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeJson<T>(filename: string, data: T): void {
|
|
115
|
+
ensureDir();
|
|
116
|
+
const filepath = join(SIDEBAR_DIR, filename);
|
|
117
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Read/write helpers for PROJECT-SPECIFIC data
|
|
121
|
+
function readProjectJson<T>(filename: string, defaultValue: T): T {
|
|
122
|
+
const projectDir = getProjectDir();
|
|
123
|
+
const filepath = join(projectDir, filename);
|
|
124
|
+
try {
|
|
125
|
+
if (existsSync(filepath)) {
|
|
126
|
+
return JSON.parse(readFileSync(filepath, "utf-8"));
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Return default on error
|
|
130
|
+
}
|
|
131
|
+
return defaultValue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeProjectJson<T>(filename: string, data: T): void {
|
|
135
|
+
const projectDir = getProjectDir();
|
|
136
|
+
mkdirSync(projectDir, { recursive: true });
|
|
137
|
+
updateProjectMapping();
|
|
138
|
+
const filepath = join(projectDir, filename);
|
|
139
|
+
writeFileSync(filepath, JSON.stringify(data, null, 2));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Tasks (user's task queue) - PROJECT SPECIFIC
|
|
143
|
+
export function getTasks(): Task[] {
|
|
144
|
+
return readProjectJson<Task[]>("tasks.json", []);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function setTasks(tasks: Task[]): void {
|
|
148
|
+
writeProjectJson("tasks.json", tasks);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function addTask(
|
|
152
|
+
content: string,
|
|
153
|
+
options?: {
|
|
154
|
+
clarified?: boolean;
|
|
155
|
+
priority?: number;
|
|
156
|
+
recommended?: boolean;
|
|
157
|
+
planPath?: string;
|
|
158
|
+
}
|
|
159
|
+
): Task {
|
|
160
|
+
const tasks = getTasks();
|
|
161
|
+
const task: Task = {
|
|
162
|
+
id: crypto.randomUUID(),
|
|
163
|
+
content,
|
|
164
|
+
createdAt: new Date().toISOString(),
|
|
165
|
+
clarified: options?.clarified,
|
|
166
|
+
priority: options?.priority,
|
|
167
|
+
recommended: options?.recommended,
|
|
168
|
+
planPath: options?.planPath,
|
|
169
|
+
};
|
|
170
|
+
tasks.push(task);
|
|
171
|
+
setTasks(tasks);
|
|
172
|
+
return task;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function updateTask(id: string, content: string): void {
|
|
176
|
+
const tasks = getTasks();
|
|
177
|
+
const task = tasks.find((t) => t.id === id);
|
|
178
|
+
if (task) {
|
|
179
|
+
task.content = content;
|
|
180
|
+
setTasks(tasks);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function markTaskClarified(id: string): void {
|
|
185
|
+
const tasks = getTasks();
|
|
186
|
+
const task = tasks.find((t) => t.id === id);
|
|
187
|
+
if (task) {
|
|
188
|
+
task.clarified = true;
|
|
189
|
+
setTasks(tasks);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function removeTask(id: string): void {
|
|
194
|
+
const tasks = getTasks().filter((t) => t.id !== id);
|
|
195
|
+
setTasks(tasks);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Active task (currently being worked on by Claude) - PROJECT SPECIFIC
|
|
199
|
+
export function getActiveTask(): ActiveTask | null {
|
|
200
|
+
return readProjectJson<ActiveTask | null>("active.json", null);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Statusline data (from Claude Code)
|
|
204
|
+
export function getStatusline(): StatuslineData | null {
|
|
205
|
+
return readJson<StatuslineData | null>("statusline.json", null);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Claude's todo list (from TodoWrite hook)
|
|
209
|
+
export function getClaudeTodos(): ClaudeTodosData | null {
|
|
210
|
+
return readJson<ClaudeTodosData | null>("claude-todos.json", null);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Claude Code config (plugins and MCPs)
|
|
214
|
+
export function getClaudeConfig(): ClaudeConfig {
|
|
215
|
+
const claudeSettingsPath = join(homedir(), ".claude", "settings.json");
|
|
216
|
+
const defaultConfig: ClaudeConfig = { enabledPlugins: [], mcpServers: [] };
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
if (existsSync(claudeSettingsPath)) {
|
|
220
|
+
const settings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
|
|
221
|
+
|
|
222
|
+
// Extract enabled plugins (keys where value is true)
|
|
223
|
+
const enabledPlugins: string[] = settings.enabledPlugins
|
|
224
|
+
? Object.entries(settings.enabledPlugins)
|
|
225
|
+
.filter(([_, enabled]) => enabled === true)
|
|
226
|
+
.map(([name]) => name.split("@")[0] || name)
|
|
227
|
+
: [];
|
|
228
|
+
|
|
229
|
+
// Extract MCP server names
|
|
230
|
+
const mcpServers: string[] = settings.mcpServers
|
|
231
|
+
? Object.keys(settings.mcpServers)
|
|
232
|
+
: [];
|
|
233
|
+
|
|
234
|
+
return { enabledPlugins, mcpServers };
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Return default on error
|
|
238
|
+
}
|
|
239
|
+
return defaultConfig;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function setActiveTask(task: Task | null): void {
|
|
243
|
+
if (task) {
|
|
244
|
+
writeProjectJson("active.json", {
|
|
245
|
+
id: task.id,
|
|
246
|
+
content: task.content,
|
|
247
|
+
sentAt: new Date().toISOString(),
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
writeProjectJson("active.json", null);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function clearActiveTask(): void {
|
|
255
|
+
writeProjectJson("active.json", null);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// History (completed tasks - append-only log) - PROJECT SPECIFIC
|
|
259
|
+
export function appendToHistory(content: string): void {
|
|
260
|
+
const projectDir = getProjectDir();
|
|
261
|
+
mkdirSync(projectDir, { recursive: true });
|
|
262
|
+
const filepath = join(projectDir, "history.log");
|
|
263
|
+
const timestamp = new Date().toISOString();
|
|
264
|
+
const entry = `${timestamp} | ${content}\n`;
|
|
265
|
+
appendFileSync(filepath, entry);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Move task from queue to active - PROJECT SPECIFIC
|
|
269
|
+
export function activateTask(taskId: string): ActiveTask | null {
|
|
270
|
+
const tasks = getTasks();
|
|
271
|
+
const taskIndex = tasks.findIndex((t) => t.id === taskId);
|
|
272
|
+
const task = tasks[taskIndex];
|
|
273
|
+
|
|
274
|
+
if (taskIndex === -1 || !task) return null;
|
|
275
|
+
|
|
276
|
+
// Remove from queue
|
|
277
|
+
tasks.splice(taskIndex, 1);
|
|
278
|
+
setTasks(tasks);
|
|
279
|
+
|
|
280
|
+
// Set as active
|
|
281
|
+
const activeTask: ActiveTask = {
|
|
282
|
+
id: task.id,
|
|
283
|
+
content: task.content,
|
|
284
|
+
sentAt: new Date().toISOString(),
|
|
285
|
+
};
|
|
286
|
+
writeProjectJson("active.json", activeTask);
|
|
287
|
+
|
|
288
|
+
return activeTask;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Recently done tasks - PROJECT SPECIFIC
|
|
292
|
+
export function getRecentlyDone(): DoneTask[] {
|
|
293
|
+
return readProjectJson<DoneTask[]>("done.json", []);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function addToDone(task: { id: string; content: string }): void {
|
|
297
|
+
const done = getRecentlyDone();
|
|
298
|
+
const doneTask: DoneTask = {
|
|
299
|
+
id: task.id,
|
|
300
|
+
content: task.content,
|
|
301
|
+
completedAt: new Date().toISOString(),
|
|
302
|
+
};
|
|
303
|
+
// Keep only last 10 done tasks
|
|
304
|
+
done.unshift(doneTask);
|
|
305
|
+
if (done.length > 10) done.pop();
|
|
306
|
+
writeProjectJson("done.json", done);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function removeFromDone(id: string): void {
|
|
310
|
+
const done = getRecentlyDone().filter((t) => t.id !== id);
|
|
311
|
+
writeProjectJson("done.json", done);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Return a done task back to active (user says "not done yet")
|
|
315
|
+
export function returnToActive(id: string): void {
|
|
316
|
+
const done = getRecentlyDone();
|
|
317
|
+
const task = done.find((t) => t.id === id);
|
|
318
|
+
if (task) {
|
|
319
|
+
// Remove from done
|
|
320
|
+
writeProjectJson("done.json", done.filter((t) => t.id !== id));
|
|
321
|
+
// Set as active
|
|
322
|
+
writeProjectJson("active.json", {
|
|
323
|
+
id: task.id,
|
|
324
|
+
content: task.content,
|
|
325
|
+
sentAt: new Date().toISOString(),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Complete active task (move to done list and history)
|
|
331
|
+
export function completeActiveTask(): void {
|
|
332
|
+
const active = getActiveTask();
|
|
333
|
+
if (active) {
|
|
334
|
+
addToDone(active);
|
|
335
|
+
appendToHistory(active.content);
|
|
336
|
+
clearActiveTask();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Socket path for IPC
|
|
341
|
+
export function getSocketPath(): string {
|
|
342
|
+
return join(SIDEBAR_DIR, "sidebar.sock");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Export the directory path
|
|
346
|
+
export { SIDEBAR_DIR };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Standalone input test - completely isolated from sidebar code
|
|
4
|
+
* Tests if basic terminal input causes flicker
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const CSI = '\x1b[';
|
|
8
|
+
|
|
9
|
+
// Clear screen, draw a simple UI
|
|
10
|
+
process.stdout.write(`${CSI}2J${CSI}H`);
|
|
11
|
+
process.stdout.write(`${CSI}?25l`); // Hide cursor initially
|
|
12
|
+
|
|
13
|
+
// Draw simple box
|
|
14
|
+
const width = process.stdout.columns || 50;
|
|
15
|
+
const height = process.stdout.rows || 20;
|
|
16
|
+
|
|
17
|
+
function drawUI(inputText: string = '', inputMode: boolean = false) {
|
|
18
|
+
let output = `${CSI}?2026h`; // Begin sync
|
|
19
|
+
output += `${CSI}H`; // Home
|
|
20
|
+
|
|
21
|
+
output += ` Simple Input Test\n`;
|
|
22
|
+
output += ` ─────────────────\n`;
|
|
23
|
+
output += `\n`;
|
|
24
|
+
output += ` Status: ${inputMode ? 'INPUT MODE' : 'Normal'}\n`;
|
|
25
|
+
output += `\n`;
|
|
26
|
+
output += ` Text: [${inputText}]${' '.repeat(Math.max(0, width - inputText.length - 12))}\n`;
|
|
27
|
+
output += `\n`;
|
|
28
|
+
output += ` Press 'i' to enter input mode\n`;
|
|
29
|
+
output += ` Press 'Esc' to exit input mode or quit\n`;
|
|
30
|
+
|
|
31
|
+
if (inputMode) {
|
|
32
|
+
output += `${CSI}?25h`; // Show cursor
|
|
33
|
+
} else {
|
|
34
|
+
output += `${CSI}?25l`; // Hide cursor
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
output += `${CSI}?2026l`; // End sync
|
|
38
|
+
process.stdout.write(output);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Initial draw
|
|
42
|
+
drawUI();
|
|
43
|
+
|
|
44
|
+
// Setup input
|
|
45
|
+
process.stdin.setRawMode(true);
|
|
46
|
+
process.stdin.resume();
|
|
47
|
+
|
|
48
|
+
let inputMode = false;
|
|
49
|
+
let inputBuffer = '';
|
|
50
|
+
|
|
51
|
+
process.stdin.on('data', (data) => {
|
|
52
|
+
const key = data.toString();
|
|
53
|
+
|
|
54
|
+
if (inputMode) {
|
|
55
|
+
// In input mode
|
|
56
|
+
if (key === '\x1b') {
|
|
57
|
+
// Escape - exit input mode
|
|
58
|
+
inputMode = false;
|
|
59
|
+
drawUI(inputBuffer, false);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (key === '\r' || key === '\n') {
|
|
64
|
+
// Enter - submit
|
|
65
|
+
inputMode = false;
|
|
66
|
+
drawUI(inputBuffer, false);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (key === '\x7f') {
|
|
71
|
+
// Backspace
|
|
72
|
+
if (inputBuffer.length > 0) {
|
|
73
|
+
inputBuffer = inputBuffer.slice(0, -1);
|
|
74
|
+
// Just update the text line, not full redraw
|
|
75
|
+
process.stdout.write(`${CSI}?2026h${CSI}6;10H[${inputBuffer}]${' '.repeat(20)}${CSI}?2026l`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Regular char
|
|
81
|
+
if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
|
|
82
|
+
inputBuffer += key;
|
|
83
|
+
// Just update the text line
|
|
84
|
+
process.stdout.write(`${CSI}?2026h${CSI}6;10H[${inputBuffer}]${CSI}?2026l`);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Normal mode
|
|
88
|
+
if (key === '\x1b') {
|
|
89
|
+
// Escape - quit
|
|
90
|
+
process.stdout.write(`${CSI}?25h${CSI}0m${CSI}2J${CSI}H`);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (key === 'i') {
|
|
95
|
+
// Enter input mode
|
|
96
|
+
inputMode = true;
|
|
97
|
+
inputBuffer = '';
|
|
98
|
+
drawUI('', true);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|