vibepulse 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -13
- package/bin/vibepulse.js +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/session-status-detection.md +258 -0
- package/next.config.ts +11 -0
- package/package.json +17 -11
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/readme-cover.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/opencode-config/route.ts +304 -0
- package/src/app/api/opencode-config/status/route.ts +31 -0
- package/src/app/api/opencode-events/route.ts +86 -0
- package/src/app/api/opencode-models/route.test.ts +135 -0
- package/src/app/api/opencode-models/route.ts +58 -0
- package/src/app/api/profiles/[id]/apply/route.ts +49 -0
- package/src/app/api/profiles/[id]/route.ts +160 -0
- package/src/app/api/profiles/route.ts +107 -0
- package/src/app/api/sessions/[id]/archive/route.ts +35 -0
- package/src/app/api/sessions/[id]/delete/route.ts +26 -0
- package/src/app/api/sessions/[id]/route.ts +45 -0
- package/src/app/api/sessions/route.ts +596 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +66 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/page.tsx +239 -0
- package/src/components/ErrorBoundary.tsx +72 -0
- package/src/components/KanbanBoard.tsx +442 -0
- package/src/components/LoadingState.tsx +37 -0
- package/src/components/ProjectCard.tsx +382 -0
- package/src/components/QueryProvider.tsx +25 -0
- package/src/components/SessionCard.tsx +291 -0
- package/src/components/SessionList.tsx +60 -0
- package/src/components/opencode-config/AgentConfigForm.test.tsx +66 -0
- package/src/components/opencode-config/AgentConfigForm.tsx +445 -0
- package/src/components/opencode-config/AgentModelSelector.tsx +284 -0
- package/src/components/opencode-config/AgentsConfigPanel.tsx +162 -0
- package/src/components/opencode-config/ConfigButton.tsx +43 -0
- package/src/components/opencode-config/ConfigPanel.tsx +91 -0
- package/src/components/opencode-config/FullscreenConfigPanel.tsx +360 -0
- package/src/components/opencode-config/categories/CategoriesList.tsx +328 -0
- package/src/components/opencode-config/categories/CategoriesManager.test.tsx +97 -0
- package/src/components/opencode-config/categories/CategoriesManager.tsx +174 -0
- package/src/components/opencode-config/categories/CategoryConfigForm.tsx +384 -0
- package/src/components/opencode-config/profiles/ProfileCard.tsx +140 -0
- package/src/components/opencode-config/profiles/ProfileEditor.tsx +446 -0
- package/src/components/opencode-config/profiles/ProfileList.tsx +398 -0
- package/src/components/opencode-config/profiles/ProfileManager.test.tsx +122 -0
- package/src/components/opencode-config/profiles/ProfileManager.tsx +293 -0
- package/src/components/ui/Tabs.tsx +59 -0
- package/src/hooks/useOpencodeSync.ts +378 -0
- package/src/index.ts +2 -0
- package/src/lib/notificationSound.ts +266 -0
- package/src/lib/opencodeConfig.test.ts +81 -0
- package/src/lib/opencodeConfig.ts +48 -0
- package/src/lib/opencodeDiscovery.ts +154 -0
- package/src/lib/profiles/storage.ts +264 -0
- package/src/lib/transform.ts +84 -0
- package/src/test/setup.ts +8 -0
- package/src/types/index.ts +89 -0
- package/src/types/opencodeConfig.ts +133 -0
- package/src/types/testing-library-vitest.d.ts +17 -0
- package/tsconfig.json +34 -0
- package/tsconfig.lib.json +17 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { readConfig, writeConfig } from './opencodeConfig';
|
|
3
|
+
import { writeFile, unlink, mkdir } from 'fs/promises';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
|
|
8
|
+
const TEST_CONFIG_DIR = join(tmpdir(), 'vibepulse-test-' + Date.now());
|
|
9
|
+
const TEST_CONFIG_PATH = join(TEST_CONFIG_DIR, 'oh-my-opencode.json');
|
|
10
|
+
|
|
11
|
+
async function cleanup() {
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(TEST_CONFIG_PATH)) {
|
|
14
|
+
await unlink(TEST_CONFIG_PATH);
|
|
15
|
+
}
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('opencodeConfig', () => {
|
|
20
|
+
beforeEach(cleanup);
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
describe('config echo bug fixes', () => {
|
|
24
|
+
it('should correctly read config immediately after saving', async () => {
|
|
25
|
+
const originalConfig = {
|
|
26
|
+
agents: {
|
|
27
|
+
sisyphus: {
|
|
28
|
+
model: 'claude-sonnet-4-20250514',
|
|
29
|
+
variant: 'high',
|
|
30
|
+
temperature: 0.2,
|
|
31
|
+
top_p: 0.9,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
categories: {
|
|
35
|
+
coding: { model: 'gpt-4', variant: 'high' },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
await writeConfig(originalConfig, TEST_CONFIG_PATH);
|
|
40
|
+
const echoed = await readConfig(TEST_CONFIG_PATH);
|
|
41
|
+
|
|
42
|
+
expect(echoed).toEqual(originalConfig);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should not lose other fields during partial update', async () => {
|
|
46
|
+
await writeConfig({
|
|
47
|
+
agents: {
|
|
48
|
+
sisyphus: { model: 'claude', temperature: 0.5 },
|
|
49
|
+
prometheus: { model: 'gpt-4', temperature: 0.7 },
|
|
50
|
+
},
|
|
51
|
+
}, TEST_CONFIG_PATH);
|
|
52
|
+
|
|
53
|
+
const loaded = await readConfig(TEST_CONFIG_PATH);
|
|
54
|
+
const updated = {
|
|
55
|
+
...loaded,
|
|
56
|
+
agents: {
|
|
57
|
+
...loaded.agents,
|
|
58
|
+
sisyphus: { ...loaded.agents?.sisyphus, temperature: 0.9 },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await writeConfig(updated, TEST_CONFIG_PATH);
|
|
63
|
+
const final = await readConfig(TEST_CONFIG_PATH);
|
|
64
|
+
|
|
65
|
+
expect(final.agents?.prometheus).toEqual({ model: 'gpt-4', temperature: 0.7 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return empty object when file does not exist', async () => {
|
|
69
|
+
const config = await readConfig(TEST_CONFIG_PATH);
|
|
70
|
+
expect(config).toEqual({});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return empty object for invalid JSON', async () => {
|
|
74
|
+
await mkdir(TEST_CONFIG_DIR, { recursive: true });
|
|
75
|
+
await writeFile(TEST_CONFIG_PATH, 'invalid {{{ json');
|
|
76
|
+
|
|
77
|
+
const config = await readConfig(TEST_CONFIG_PATH);
|
|
78
|
+
expect(config).toEqual({});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { parse, stringify } from 'comment-json';
|
|
6
|
+
|
|
7
|
+
export const CONFIG_DIR = join(homedir(), '.config', 'opencode');
|
|
8
|
+
export const CONFIG_PATH = join(CONFIG_DIR, 'oh-my-opencode.json');
|
|
9
|
+
|
|
10
|
+
export type OpenCodeConfig = {
|
|
11
|
+
agents?: Record<string, unknown>;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function detectConfig(configPath: string = CONFIG_PATH): boolean {
|
|
16
|
+
try {
|
|
17
|
+
return existsSync(configPath);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readConfig(configPath: string = CONFIG_PATH): Promise<OpenCodeConfig> {
|
|
24
|
+
try {
|
|
25
|
+
const content = await readFile(configPath, 'utf-8');
|
|
26
|
+
const config = parse(content, null, false) as OpenCodeConfig;
|
|
27
|
+
return config;
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function writeConfig(
|
|
34
|
+
config: OpenCodeConfig,
|
|
35
|
+
configPath: string = CONFIG_PATH
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
try {
|
|
38
|
+
const configDir = join(configPath, '..');
|
|
39
|
+
if (!existsSync(configDir)) {
|
|
40
|
+
mkdirSync(configDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = stringify(config, null, 2);
|
|
44
|
+
await writeFile(configPath, content, 'utf-8');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`Failed to write config: ${error}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const knownPorts = new Set<number>();
|
|
4
|
+
|
|
5
|
+
export type OpencodeProcessCwd = {
|
|
6
|
+
pid: number;
|
|
7
|
+
cwd: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function toUniqueSortedPorts(ports: number[]): number[] {
|
|
11
|
+
return Array.from(
|
|
12
|
+
new Set(ports.filter((port) => Number.isInteger(port) && port > 0 && port <= 65535))
|
|
13
|
+
).sort((a, b) => a - b);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getPortsFromLsof(): number[] {
|
|
17
|
+
try {
|
|
18
|
+
const output = execSync('lsof -nP -iTCP -sTCP:LISTEN', {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
21
|
+
});
|
|
22
|
+
const lines = output.split('\n');
|
|
23
|
+
const ports: number[] = [];
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith('COMMAND')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parts = trimmed.split(/\s+/);
|
|
32
|
+
const command = parts[0]?.toLowerCase();
|
|
33
|
+
if (command !== 'opencode') {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const match = trimmed.match(/:(\d+)\s+\(LISTEN\)/);
|
|
38
|
+
if (!match) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const port = parseInt(match[1], 10);
|
|
43
|
+
if (Number.isFinite(port)) {
|
|
44
|
+
ports.push(port);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ports;
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getPortsFromProcessArgs(): number[] {
|
|
55
|
+
try {
|
|
56
|
+
const output = execSync('ps -axo command', {
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
59
|
+
});
|
|
60
|
+
const matches = [...output.matchAll(/\bopencode\b[^\n]*\b--port(?:=|\s+)(\d+)\b/g)];
|
|
61
|
+
return matches
|
|
62
|
+
.map((match) => parseInt(match[1], 10))
|
|
63
|
+
.filter((port) => Number.isFinite(port));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function discoverOpencodePorts(): number[] {
|
|
70
|
+
const discoveredPorts = toUniqueSortedPorts([
|
|
71
|
+
...getPortsFromLsof(),
|
|
72
|
+
...getPortsFromProcessArgs(),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
for (const port of discoveredPorts) {
|
|
76
|
+
knownPorts.add(port);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return toUniqueSortedPorts([
|
|
80
|
+
...discoveredPorts,
|
|
81
|
+
...Array.from(knownPorts),
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getOpencodePidsWithoutPortFlag(): number[] {
|
|
86
|
+
try {
|
|
87
|
+
const output = execSync('ps -axo pid=,command=', {
|
|
88
|
+
encoding: 'utf-8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const pids: number[] = [];
|
|
93
|
+
const lines = output.split('\n');
|
|
94
|
+
|
|
95
|
+
for (const line of lines) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed) continue;
|
|
98
|
+
|
|
99
|
+
const match = trimmed.match(/^(\d+)\s+(.+)$/);
|
|
100
|
+
if (!match) continue;
|
|
101
|
+
|
|
102
|
+
const pid = parseInt(match[1], 10);
|
|
103
|
+
const command = match[2];
|
|
104
|
+
|
|
105
|
+
if (!Number.isFinite(pid)) continue;
|
|
106
|
+
if (!/\bopencode\b/.test(command)) continue;
|
|
107
|
+
if (/\b--port(?:=|\s+)\d+\b/.test(command)) continue;
|
|
108
|
+
|
|
109
|
+
pids.push(pid);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Array.from(new Set(pids));
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getCwdForPid(pid: number): string | null {
|
|
119
|
+
try {
|
|
120
|
+
const output = execSync(`lsof -nP -a -p ${pid} -d cwd -Fn`, {
|
|
121
|
+
encoding: 'utf-8',
|
|
122
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const cwdLine = output
|
|
126
|
+
.split('\n')
|
|
127
|
+
.find((line) => line.startsWith('n') && line.length > 1);
|
|
128
|
+
|
|
129
|
+
if (!cwdLine) return null;
|
|
130
|
+
return cwdLine.slice(1);
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function discoverOpencodeProcessCwdsWithoutPort(): OpencodeProcessCwd[] {
|
|
137
|
+
const pids = getOpencodePidsWithoutPortFlag();
|
|
138
|
+
if (!pids.length) return [];
|
|
139
|
+
|
|
140
|
+
const processes: OpencodeProcessCwd[] = [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
|
|
143
|
+
for (const pid of pids) {
|
|
144
|
+
const cwd = getCwdForPid(pid);
|
|
145
|
+
if (!cwd) continue;
|
|
146
|
+
|
|
147
|
+
const key = `${pid}:${cwd}`;
|
|
148
|
+
if (seen.has(key)) continue;
|
|
149
|
+
seen.add(key);
|
|
150
|
+
processes.push({ pid, cwd });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return processes;
|
|
154
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { readFile, writeFile, unlink } from 'fs/promises';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { parse, stringify } from 'comment-json';
|
|
6
|
+
import type { AgentConfig, CategoryConfig } from '@/types/opencodeConfig';
|
|
7
|
+
|
|
8
|
+
export const PROFILES_DIR = join(homedir(), '.config', 'opencode', 'profiles');
|
|
9
|
+
export const PROFILE_INDEX_PATH = join(PROFILES_DIR, 'index.json');
|
|
10
|
+
|
|
11
|
+
export interface Profile {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
emoji: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
isDefault?: boolean;
|
|
19
|
+
isBuiltIn?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProfileIndex {
|
|
23
|
+
version: number;
|
|
24
|
+
profiles: Profile[];
|
|
25
|
+
activeProfileId: string | null;
|
|
26
|
+
lastModified: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProfileConfig {
|
|
30
|
+
agents: Record<string, AgentConfig>;
|
|
31
|
+
categories?: Record<string, CategoryConfig>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BUILTIN_PROFILES: Profile[] = [
|
|
35
|
+
{
|
|
36
|
+
id: 'balanced',
|
|
37
|
+
name: 'Balanced',
|
|
38
|
+
emoji: '⚖️',
|
|
39
|
+
description: 'Balanced multi-model orchestration optimized for general coding tasks',
|
|
40
|
+
createdAt: new Date().toISOString(),
|
|
41
|
+
updatedAt: new Date().toISOString(),
|
|
42
|
+
isBuiltIn: true,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const BUILTIN_PROFILE_CONFIGS: Record<string, ProfileConfig> = {
|
|
47
|
+
balanced: {
|
|
48
|
+
agents: {
|
|
49
|
+
sisyphus: {
|
|
50
|
+
model: 'kimi-for-coding/k2p5',
|
|
51
|
+
variant: 'high',
|
|
52
|
+
temperature: 0.2,
|
|
53
|
+
top_p: 0.9,
|
|
54
|
+
},
|
|
55
|
+
oracle: {
|
|
56
|
+
model: 'openai/gpt-5.4',
|
|
57
|
+
variant: 'high',
|
|
58
|
+
temperature: 0.2,
|
|
59
|
+
top_p: 0.9,
|
|
60
|
+
},
|
|
61
|
+
librarian: {
|
|
62
|
+
model: 'google/gemini-3-flash',
|
|
63
|
+
temperature: 0.3,
|
|
64
|
+
top_p: 0.9,
|
|
65
|
+
},
|
|
66
|
+
explore: {
|
|
67
|
+
model: 'github-copilot/grok-code-fast-1',
|
|
68
|
+
temperature: 0.1,
|
|
69
|
+
top_p: 0.9,
|
|
70
|
+
},
|
|
71
|
+
'multimodal-looker': {
|
|
72
|
+
model: 'kimi-for-coding/k2p5',
|
|
73
|
+
temperature: 0.2,
|
|
74
|
+
top_p: 0.9,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
categories: {
|
|
78
|
+
'visual-engineering': {
|
|
79
|
+
model: 'google/gemini-3.1-pro',
|
|
80
|
+
variant: 'high',
|
|
81
|
+
},
|
|
82
|
+
deep: {
|
|
83
|
+
model: 'openai/gpt-5.3-codex',
|
|
84
|
+
variant: 'medium',
|
|
85
|
+
},
|
|
86
|
+
quick: {
|
|
87
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
88
|
+
temperature: 0.1,
|
|
89
|
+
},
|
|
90
|
+
'unspecified-low': {
|
|
91
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
92
|
+
temperature: 0.2,
|
|
93
|
+
},
|
|
94
|
+
'unspecified-high': {
|
|
95
|
+
model: 'openai/gpt-5.4',
|
|
96
|
+
variant: 'high',
|
|
97
|
+
temperature: 0.2,
|
|
98
|
+
},
|
|
99
|
+
writing: {
|
|
100
|
+
model: 'kimi-for-coding/k2p5',
|
|
101
|
+
temperature: 0.3,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function ensureProfilesDir(): void {
|
|
108
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
109
|
+
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getProfileConfigPath(id: string): string {
|
|
114
|
+
return join(PROFILES_DIR, `${id}.json`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createDefaultProfileIndex(): ProfileIndex {
|
|
118
|
+
return {
|
|
119
|
+
version: 1,
|
|
120
|
+
profiles: [...BUILTIN_PROFILES],
|
|
121
|
+
activeProfileId: null,
|
|
122
|
+
lastModified: new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function createBuiltinProfileConfigs(): Promise<void> {
|
|
127
|
+
const validBuiltinIds = new Set(BUILTIN_PROFILES.map(p => p.id));
|
|
128
|
+
const deprecatedBuiltinIds = ['coding', 'writing', 'debug', 'minimal'];
|
|
129
|
+
|
|
130
|
+
for (const deprecatedId of deprecatedBuiltinIds) {
|
|
131
|
+
if (!validBuiltinIds.has(deprecatedId)) {
|
|
132
|
+
const deprecatedConfigPath = getProfileConfigPath(deprecatedId);
|
|
133
|
+
if (existsSync(deprecatedConfigPath)) {
|
|
134
|
+
await unlink(deprecatedConfigPath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const profile of BUILTIN_PROFILES) {
|
|
140
|
+
const configPath = getProfileConfigPath(profile.id);
|
|
141
|
+
const config = BUILTIN_PROFILE_CONFIGS[profile.id] || { agents: {} };
|
|
142
|
+
const content = stringify(config, null, 2);
|
|
143
|
+
await writeFile(configPath, content, 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function readProfileIndex(): Promise<ProfileIndex> {
|
|
148
|
+
try {
|
|
149
|
+
ensureProfilesDir();
|
|
150
|
+
|
|
151
|
+
if (!existsSync(PROFILE_INDEX_PATH)) {
|
|
152
|
+
const defaultIndex = createDefaultProfileIndex();
|
|
153
|
+
await writeProfileIndex(defaultIndex);
|
|
154
|
+
await createBuiltinProfileConfigs();
|
|
155
|
+
return defaultIndex;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const content = await readFile(PROFILE_INDEX_PATH, 'utf-8');
|
|
159
|
+
const index = parse(content, null, false) as unknown as ProfileIndex;
|
|
160
|
+
|
|
161
|
+
// Ensure all built-in profiles exist in the index
|
|
162
|
+
let modified = false;
|
|
163
|
+
for (const builtinProfile of BUILTIN_PROFILES) {
|
|
164
|
+
const exists = index.profiles.some(p => p.id === builtinProfile.id);
|
|
165
|
+
if (!exists) {
|
|
166
|
+
index.profiles.push(builtinProfile);
|
|
167
|
+
modified = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove built-in profiles that are no longer in BUILTIN_PROFILES
|
|
172
|
+
const builtinIds = new Set(BUILTIN_PROFILES.map(p => p.id));
|
|
173
|
+
const oldLength = index.profiles.length;
|
|
174
|
+
index.profiles = index.profiles.filter(p => !p.isBuiltIn || builtinIds.has(p.id));
|
|
175
|
+
if (index.profiles.length !== oldLength) {
|
|
176
|
+
modified = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await createBuiltinProfileConfigs();
|
|
180
|
+
|
|
181
|
+
if (modified) {
|
|
182
|
+
index.lastModified = new Date().toISOString();
|
|
183
|
+
await writeProfileIndex(index);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return index;
|
|
187
|
+
} catch {
|
|
188
|
+
return createDefaultProfileIndex();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function writeProfileIndex(index: ProfileIndex): Promise<void> {
|
|
193
|
+
ensureProfilesDir();
|
|
194
|
+
|
|
195
|
+
index.lastModified = new Date().toISOString();
|
|
196
|
+
const content = stringify(index, null, 2);
|
|
197
|
+
|
|
198
|
+
await writeFile(PROFILE_INDEX_PATH, content, 'utf-8');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
export async function readProfileConfig(id: string): Promise<ProfileConfig> {
|
|
203
|
+
try {
|
|
204
|
+
const configPath = getProfileConfigPath(id);
|
|
205
|
+
|
|
206
|
+
if (!existsSync(configPath)) {
|
|
207
|
+
return { agents: {} };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const content = await readFile(configPath, 'utf-8');
|
|
211
|
+
const config = parse(content, null, false) as unknown as ProfileConfig;
|
|
212
|
+
return config;
|
|
213
|
+
} catch {
|
|
214
|
+
return { agents: {} };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function writeProfileConfig(
|
|
219
|
+
id: string,
|
|
220
|
+
config: ProfileConfig
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
ensureProfilesDir();
|
|
223
|
+
|
|
224
|
+
const configPath = getProfileConfigPath(id);
|
|
225
|
+
const content = stringify(config, null, 2);
|
|
226
|
+
|
|
227
|
+
await writeFile(configPath, content, 'utf-8');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function deleteProfileConfig(id: string): Promise<boolean> {
|
|
231
|
+
const index = await readProfileIndex();
|
|
232
|
+
const profile = index.profiles.find(p => p.id === id);
|
|
233
|
+
|
|
234
|
+
if (profile?.isBuiltIn) {
|
|
235
|
+
const defaultConfig = BUILTIN_PROFILE_CONFIGS[id] || { agents: {} };
|
|
236
|
+
await writeProfileConfig(id, defaultConfig);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const configPath = getProfileConfigPath(id);
|
|
241
|
+
|
|
242
|
+
if (!existsSync(configPath)) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await unlink(configPath);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function getProfileById(id: string): Promise<Profile | undefined> {
|
|
251
|
+
const index = await readProfileIndex();
|
|
252
|
+
return index.profiles.find(p => p.id === id);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getActiveProfileId(): Promise<string | null> {
|
|
256
|
+
const index = await readProfileIndex();
|
|
257
|
+
return index.activeProfileId;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function setActiveProfileId(id: string | null): Promise<void> {
|
|
261
|
+
const index = await readProfileIndex();
|
|
262
|
+
index.activeProfileId = id;
|
|
263
|
+
await writeProfileIndex(index);
|
|
264
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { KanbanCard, OpencodeSession, KanbanColumn } from '@/types';
|
|
2
|
+
|
|
3
|
+
interface EnrichedSession extends OpencodeSession {
|
|
4
|
+
realTimeStatus?: 'idle' | 'busy' | 'retry';
|
|
5
|
+
projectName?: string;
|
|
6
|
+
branch?: string;
|
|
7
|
+
waitingForUser?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function transformSession(session: EnrichedSession): KanbanCard {
|
|
11
|
+
let status: KanbanColumn;
|
|
12
|
+
|
|
13
|
+
const realTimeStatus = session.realTimeStatus || 'idle';
|
|
14
|
+
const hasActiveChildren = (session.children || []).some((child) => {
|
|
15
|
+
const childStatus = child.realTimeStatus || 'idle';
|
|
16
|
+
return childStatus === 'busy' || childStatus === 'retry';
|
|
17
|
+
});
|
|
18
|
+
const effectiveStatus =
|
|
19
|
+
realTimeStatus === 'retry'
|
|
20
|
+
? 'retry'
|
|
21
|
+
: (realTimeStatus === 'busy' || hasActiveChildren)
|
|
22
|
+
? 'busy'
|
|
23
|
+
: 'idle';
|
|
24
|
+
const hasWaitingChildren = (session.children || []).some((child) => {
|
|
25
|
+
const childStatus = child.realTimeStatus || 'idle';
|
|
26
|
+
return childStatus === 'retry' || (childStatus !== 'idle' && !!child.waitingForUser);
|
|
27
|
+
});
|
|
28
|
+
const waitingForUser =
|
|
29
|
+
effectiveStatus === 'retry' ||
|
|
30
|
+
(effectiveStatus === 'busy' && (!!session.waitingForUser || hasWaitingChildren));
|
|
31
|
+
|
|
32
|
+
if (session.time?.archived) {
|
|
33
|
+
status = 'done';
|
|
34
|
+
} else if (waitingForUser) {
|
|
35
|
+
status = 'review'; // Needs Attention
|
|
36
|
+
} else if (effectiveStatus === 'busy') {
|
|
37
|
+
status = 'busy';
|
|
38
|
+
} else {
|
|
39
|
+
status = 'idle';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: session.id,
|
|
44
|
+
sessionSlug: session.slug,
|
|
45
|
+
title: session.title || 'Untitled Session',
|
|
46
|
+
directory: session.directory,
|
|
47
|
+
projectName: session.projectName || 'Unknown Project',
|
|
48
|
+
branch: session.branch,
|
|
49
|
+
agents: extractAgents(session.slug),
|
|
50
|
+
messageCount: session.messageCount || 0,
|
|
51
|
+
status: status,
|
|
52
|
+
opencodeStatus: effectiveStatus,
|
|
53
|
+
waitingForUser,
|
|
54
|
+
todosTotal: 0,
|
|
55
|
+
todosCompleted: 0,
|
|
56
|
+
createdAt: session.time.created,
|
|
57
|
+
updatedAt: session.time.updated,
|
|
58
|
+
archivedAt: session.time.archived,
|
|
59
|
+
sortOrder: 0,
|
|
60
|
+
children: (session.children || []).map(c => ({
|
|
61
|
+
id: c.id,
|
|
62
|
+
title: c.title,
|
|
63
|
+
realTimeStatus: c.realTimeStatus || 'idle',
|
|
64
|
+
waitingForUser:
|
|
65
|
+
(c.realTimeStatus || 'idle') === 'retry' ||
|
|
66
|
+
((c.realTimeStatus || 'idle') === 'busy' && !!c.waitingForUser),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extractAgents(slug: string): string[] {
|
|
72
|
+
// Extract agent names from session slug
|
|
73
|
+
// Slug format: session_<timestamp>_<agent1>-<agent2>...
|
|
74
|
+
const parts = slug.split('_');
|
|
75
|
+
if (parts.length >= 3) {
|
|
76
|
+
const agentsPart = parts[parts.length - 1];
|
|
77
|
+
return agentsPart.split('-').filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function transformSessions(sessions: EnrichedSession[]): KanbanCard[] {
|
|
83
|
+
return sessions.map(transformSession);
|
|
84
|
+
}
|