@adhdev/daemon-core 0.6.48 → 0.6.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.6.48",
3
+ "version": "0.6.50",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -67,9 +67,6 @@
67
67
  "patterns": {
68
68
  "prompt": [
69
69
  { "source": "Type your message", "flags": "i" },
70
- { "source": "^>\\s*$", "flags": "m" },
71
- { "source": "[›❯]\\s*[\\r\\n]", "flags": "" },
72
- { "source": "[›❯]\\s*$", "flags": "m" },
73
70
  { "source": "for\\s*shortcuts", "flags": "i" },
74
71
  { "source": "\\?\\s*for\\s*help", "flags": "i" },
75
72
  { "source": "Press enter", "flags": "i" }
@@ -85,10 +82,7 @@
85
82
  { "source": "Allow\\s*once", "flags": "i" },
86
83
  { "source": "Always\\s*allow", "flags": "i" },
87
84
  { "source": "\\(y/n\\)", "flags": "i" },
88
- { "source": "\\[Y/n\\]", "flags": "i" },
89
- { "source": "Run\\s+\\w+\\s+command", "flags": "i" },
90
- { "source": "Yes,?\\s*don'?t\\s*ask", "flags": "i" },
91
- { "source": "\\bDeny\\b", "flags": "i" }
85
+ { "source": "\\[Y/n\\]", "flags": "i" }
92
86
  ],
93
87
  "ready": [
94
88
  { "source": "for\\s*shortcuts", "flags": "i" },
@@ -39,7 +39,6 @@
39
39
  },
40
40
  "inputMethod": "cdp-type-and-send",
41
41
  "inputSelector": "[contenteditable=\"true\"][role=\"textbox\"]",
42
- "webviewMatchText": "kiro",
43
42
  "settings": {
44
43
  "approvalAlert": {
45
44
  "type": "boolean",
@@ -0,0 +1 @@
1
+ (() => { return document.documentElement.outerHTML; })()
@@ -1,34 +1,37 @@
1
1
  /**
2
- * Generic fallback list_models
2
+ * Kirowebview_list_models
3
3
  */
4
- (() => {
4
+ (async () => {
5
5
  try {
6
- const models = [];
7
- let current = '';
6
+ const trigger = document.querySelector('.kiro-dropdown-trigger');
7
+ if (!trigger) {
8
+ return JSON.stringify({ models: [], current: 'Default', error: 'No dropdown found' });
9
+ }
8
10
 
9
- // Try generic Model string from select/button
10
- const sel = document.querySelectorAll('select, [class*="model"], [id*="model"]');
11
- for (const el of sel) {
12
- const txt = (el.textContent || '').trim();
13
- if (txt && /claude|gpt|gemini|sonnet|opus/i.test(txt)) {
14
- if (txt.length < 50) {
15
- models.push(txt);
16
- if (!current) current = txt;
17
- }
18
- }
11
+ const current = (trigger.querySelector('.kiro-dropdown-selected-text')?.textContent || '').trim();
12
+
13
+ // Check if menu is already open
14
+ const wasExpanded = trigger.getAttribute('aria-expanded') === 'true';
15
+
16
+ if (!wasExpanded) {
17
+ trigger.click();
18
+ await new Promise(r => setTimeout(r, 150));
19
19
  }
20
20
 
21
- if (models.length === 0) {
22
- const btns = document.querySelectorAll('button');
23
- for (const b of btns) {
24
- const txt = (b.textContent || '').trim();
25
- if (txt && /claude|gpt|gemini|sonnet/i.test(txt) && txt.length < 30) {
26
- models.push(txt);
27
- current = txt;
28
- }
21
+ const models = [];
22
+ const items = document.querySelectorAll('.kiro-dropdown-item, [role="menuitem"], [role="option"]');
23
+ for (const item of items) {
24
+ const txt = (item.textContent || '').trim();
25
+ if (txt && txt.length < 50) {
26
+ models.push(txt);
29
27
  }
30
28
  }
31
29
 
30
+ // Close menu
31
+ if (!wasExpanded) {
32
+ trigger.click();
33
+ }
34
+
32
35
  return JSON.stringify({
33
36
  models: [...new Set(models)],
34
37
  current: current || 'Default'
@@ -1,39 +1,22 @@
1
1
  /**
2
- * Generic fallback list_models
2
+ * Kirowebview_list_modes
3
+ * Maps the Autopilot toggle to modes "Autopilot" and "Manual".
3
4
  */
4
5
  (() => {
5
6
  try {
6
- const models = [];
7
- let current = '';
8
-
9
- // Try generic Model string from select/button
10
- const sel = document.querySelectorAll('select, [class*="model"], [id*="model"]');
11
- for (const el of sel) {
12
- const txt = (el.textContent || '').trim();
13
- if (txt && /claude|gpt|gemini|sonnet|opus/i.test(txt)) {
14
- if (txt.length < 50) {
15
- models.push(txt);
16
- if (!current) current = txt;
17
- }
18
- }
7
+ const toggle = document.querySelector('#autonomy-mode-toggle-switch');
8
+ if (!toggle) {
9
+ // Fallback for older versions or if UI changed
10
+ return JSON.stringify({ modes: ['Default'], current: 'Default' });
19
11
  }
20
12
 
21
- if (models.length === 0) {
22
- const btns = document.querySelectorAll('button');
23
- for (const b of btns) {
24
- const txt = (b.textContent || '').trim();
25
- if (txt && /claude|gpt|gemini|sonnet/i.test(txt) && txt.length < 30) {
26
- models.push(txt);
27
- current = txt;
28
- }
29
- }
30
- }
13
+ const isAutopilot = toggle.checked;
31
14
 
32
15
  return JSON.stringify({
33
- models: [...new Set(models)],
34
- current: current || 'Default'
16
+ modes: ['Autopilot', 'Manual'],
17
+ current: isAutopilot ? 'Autopilot' : 'Manual'
35
18
  });
36
19
  } catch (e) {
37
- return JSON.stringify({ models: [], current: '', error: e.message });
20
+ return JSON.stringify({ modes: [], current: '', error: e.message });
38
21
  }
39
22
  })()
@@ -20,30 +20,122 @@
20
20
  msgElements.forEach((msg, idx) => {
21
21
  const roleMeta = msg.querySelector('.kiro-chat-message-role');
22
22
  const roleText = (roleMeta?.textContent || '').trim();
23
- const isKiro = roleText.toLowerCase() === 'kiro';
23
+ const isKiro = roleText.toLowerCase().includes('kiro');
24
24
  const role = isKiro ? 'assistant' : 'user';
25
25
 
26
+ let parts = [];
27
+
28
+ // A helper to traverse and build rough markdown
29
+ const parseNode = (node) => {
30
+ if (node.nodeType === Node.TEXT_NODE) {
31
+ return node.nodeValue;
32
+ }
33
+ const tag = node.tagName?.toLowerCase();
34
+ if (!tag) return '';
35
+
36
+ // Kiro Thought / Tool executions
37
+ if (tag === 'div' && node.className.includes('kiro-thought')) {
38
+ const toggle = node.querySelector('.summary')?.textContent || 'thought';
39
+ const content = Array.from(node.querySelectorAll('.details')).map(n => n.textContent).join('\n');
40
+ parts.push({ kind: 'thought', content });
41
+ return `\n<details><summary>${toggle}</summary>\n${content}\n</details>\n`;
42
+ }
43
+
44
+ // Kiro Agent Outcomes (Terminal / Command)
45
+ if (tag === 'div' && node.className.includes('agent-outcome')) {
46
+ const label = (node.querySelector('.agent-outcome-label')?.textContent || '').toLowerCase();
47
+ const pre = node.querySelector('.agent-outcome-details pre, .agent-outcome-details code');
48
+ const codeContent = (pre?.textContent || '').trim();
49
+ if (codeContent) {
50
+ const kind = label.includes('command') || label.includes('terminal') ? 'terminal' : 'tool';
51
+ // Add directly to parts since this is a structured execution
52
+ parts.push({ kind, content: codeContent });
53
+ return `\n> [${kind}] ${codeContent}\n`;
54
+ }
55
+ }
56
+
57
+ if (tag === 'pre') {
58
+ const code = node.querySelector('code');
59
+ const lang = code?.className?.replace('language-', '') || '';
60
+ return `\n\`\`\`${lang}\n${node.textContent}\n\`\`\`\n`;
61
+ }
62
+ if (tag === 'code') {
63
+ return `\`${node.textContent}\``;
64
+ }
65
+ if (tag === 'table') {
66
+ let str = '\n';
67
+ const rows = Array.from(node.querySelectorAll('tr'));
68
+ rows.forEach((row, i) => {
69
+ const cells = Array.from(row.querySelectorAll('td, th')).map(c => c.textContent.trim());
70
+ str += '| ' + cells.join(' | ') + ' |\n';
71
+ if (i === 0 && row.querySelector('th')) {
72
+ str += '|' + cells.map(() => '---').join('|') + '|\n';
73
+ }
74
+ });
75
+ return str + '\n';
76
+ }
77
+ if (tag === 'p') {
78
+ const text = Array.from(node.childNodes).map(parseNode).join('');
79
+ return text + '\n\n';
80
+ }
81
+
82
+ // Recursively parse children for span, div, etc
83
+ return Array.from(node.childNodes).map(parseNode).join('');
84
+ };
85
+
26
86
  const body = msg.querySelector('.kiro-chat-message-body');
27
87
  let content = '';
28
88
  if (body) {
29
- const markdown = body.querySelector('.kiro-chat-message-markdown');
30
- content = (markdown || body).textContent?.trim() || '';
89
+ // Parse the ENTIRE body recursively so we don't skip elements!
90
+ content = parseNode(body).trim();
91
+ }
92
+
93
+ // Fallback content parsing if recursive failed
94
+ if (!content) {
95
+ content = body?.textContent?.trim() || '';
31
96
  }
32
97
 
33
98
  if (content) {
34
- messages.push({ role, content, index: idx });
99
+ parts.push({ kind: 'text', content });
100
+ }
101
+
102
+ if (parts.length > 0) {
103
+ messages.push({ role, content: parts.map(p => p.content).join('\n'), parts });
35
104
  }
36
105
  });
37
106
 
38
- // 상태 감지
107
+ // 상태 감지 및 activeModal 추출
39
108
  let status = 'idle';
109
+ let activeModal = undefined;
40
110
 
41
111
  // "Working" / "Cancel" 버튼 → generating
42
- const workingBar = document.querySelector('.kiro-snackbar');
43
- if (workingBar && workingBar.offsetWidth > 0) {
44
- const barText = (workingBar.textContent || '').toLowerCase();
112
+ const snackbar = document.querySelector('.kiro-snackbar');
113
+ if (snackbar && snackbar.offsetWidth > 0) {
114
+ const barText = (snackbar.textContent || '').toLowerCase();
45
115
  if (barText.includes('working') || barText.includes('cancel')) {
46
116
  status = 'generating';
117
+ } else if (barText.includes('waiting') || barText.includes('input')) {
118
+ // 승인 대기 중 (waiting on your input)
119
+ const titleEl = snackbar.querySelector('.kiro-snackbar-title, .thinking-text');
120
+ const actionsEl = snackbar.querySelectorAll('.kiro-snackbar-actions button');
121
+ const buttons = Array.from(actionsEl).map(b => (b.textContent || '').trim());
122
+
123
+ if (buttons.length > 0) {
124
+ activeModal = {
125
+ title: (titleEl?.textContent || '').trim(),
126
+ buttons,
127
+ type: 'approval'
128
+ };
129
+ status = 'waiting_approval';
130
+ }
131
+ }
132
+ }
133
+
134
+ // If snackbar didn't catch it, fallback to Stop button checking
135
+ if (status === 'idle') {
136
+ const hasStopBtn = document.querySelector('.codicon-debug-stop, [aria-label*="stop" i], [title*="stop" i], [title*="cancel generation" i], .kiro-button[data-loading="true"]');
137
+ if (hasStopBtn) {
138
+ status = 'generating';
47
139
  }
48
140
  }
49
141
 
@@ -59,8 +151,9 @@
59
151
  id: title || 'kiro-default',
60
152
  status,
61
153
  messages,
62
- title: title || undefined,
63
- inputContent: inputContent || undefined,
154
+ title: title || '',
155
+ inputContent: inputContent || '',
156
+ ...(activeModal ? { activeModal } : {})
64
157
  });
65
158
  } catch (e) {
66
159
  return JSON.stringify({ id: '', status: 'error', messages: [], error: e.message });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Kiro — webview_resolve_action
3
+ * Kiro's approval dialog uses buttons in the kiro-snackbar.
4
+ * 파라미터: ${ BUTTON_TEXT }
5
+ */
6
+ (() => {
7
+ try {
8
+ const want = ${ BUTTON_TEXT };
9
+ const wantNorm = (want || '').replace(/\s+/g, ' ').trim().toLowerCase();
10
+
11
+ function matches(el) {
12
+ const t = (el.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
13
+ if (!t) return false;
14
+ if (t === wantNorm || t.startsWith(wantNorm) || wantNorm.startsWith(t)) return true;
15
+ if (/^(run|approve|allow|accept|yes|trust)\b/.test(wantNorm)) {
16
+ if (/^(run|allow|accept|approve|trust)\b/.test(t)) return true;
17
+ }
18
+ if (/^(reject|deny|no|abort|cancel)\b/.test(wantNorm)) {
19
+ if (/^(reject|deny|cancel)\b/.test(t)) return true;
20
+ }
21
+ return false;
22
+ }
23
+
24
+ const btns = Array.from(document.querySelectorAll('button, [role="button"]'));
25
+ let found = null;
26
+ for (const b of btns.slice().reverse()) {
27
+ const attrText = (b.getAttribute('title') || b.getAttribute('aria-label') || '').replace(/\s+/g, ' ').trim().toLowerCase();
28
+ const hasPlay = b.querySelector('.codicon-play, .codicon-check') !== null;
29
+ const hasReject = b.querySelector('.codicon-chrome-close, .codicon-close') !== null;
30
+
31
+ // Check direct match
32
+ if (matches(b)) {
33
+ found = b;
34
+ break;
35
+ }
36
+
37
+ // Checks for 'run/approve' intent
38
+ if (/^(run|approve|allow|accept|yes|trust)\b/.test(wantNorm)) {
39
+ if (hasPlay || attrText.includes('run') || attrText.includes('approve') || attrText.includes('allow') || attrText.includes('trust')) {
40
+ found = b;
41
+ break;
42
+ }
43
+ }
44
+
45
+ // Checks for 'reject/deny/cancel' intent
46
+ if (/^(reject|deny|no|abort|cancel)\b/.test(wantNorm)) {
47
+ if (hasReject || attrText.includes('reject') || attrText.includes('deny') || attrText.includes('cancel')) {
48
+ found = b;
49
+ break;
50
+ }
51
+ }
52
+ }
53
+
54
+ if (found) {
55
+ found.click();
56
+ return JSON.stringify({ resolved: true, method: 'webview_button_click' });
57
+ }
58
+ return JSON.stringify({ resolved: false, want: wantNorm, error: 'Button not found' });
59
+ } catch (e) {
60
+ return JSON.stringify({ resolved: false, error: e.message });
61
+ }
62
+ })()
@@ -54,7 +54,29 @@
54
54
  editor.dispatchEvent(new Event('input', { bubbles: true }));
55
55
  await new Promise(r => setTimeout(r, 400));
56
56
 
57
- // ─── 3. Enter 전송 ───
57
+ // ─── 3. 전송 버튼 클릭 (Enter 키가 먹힐 때 대비) ───
58
+ const sendBtns = Array.from(document.querySelectorAll('button, div[role="button"], span[role="button"]'))
59
+ .filter(b => {
60
+ const aria = (b.getAttribute('aria-label') || '').toLowerCase();
61
+ const title = (b.getAttribute('title') || '').toLowerCase();
62
+ const text = (b.textContent || '').toLowerCase();
63
+ const className = (b.className || '').toLowerCase();
64
+ return aria.includes('send') || aria.includes('submit') ||
65
+ title.includes('send') || title.includes('submit') ||
66
+ className.includes('send') || className.includes('submit') ||
67
+ b.querySelector('svg'); // Fallback for icon-only buttons next to input
68
+ });
69
+
70
+ // Find the button closest to the editor
71
+ let submitBtn = null;
72
+ if (sendBtns.length > 0) {
73
+ // grab the one visually right/bottom to the editor, or just the last svg button
74
+ submitBtn = sendBtns[sendBtns.length - 1];
75
+ submitBtn.click();
76
+ await new Promise(r => setTimeout(r, 100));
77
+ }
78
+
79
+ // ─── 4. Enter 키 전송 (Fallback) ───
58
80
  const enterOpts = {
59
81
  key: 'Enter', code: 'Enter',
60
82
  keyCode: 13, which: 13,
@@ -1,14 +1,33 @@
1
1
  /**
2
- * Generic fallback set_model
3
- * ${ MODEL }
2
+ * Kirowebview_set_mode
3
+ * Toggles the Autopilot switch based on requested mode.
4
+ * 파라미터: ${ MODE }
4
5
  */
5
6
  (() => {
6
7
  try {
7
- const want = ${ MODEL } || '';
8
- const norm = (t) => t.toLowerCase().trim();
8
+ const mode = ${ MODE };
9
+ if (!mode) return JSON.stringify({ success: false, error: 'No mode specified' });
9
10
 
10
- // Very basic click attempt
11
- return JSON.stringify({ success: false, error: 'Model selection requires UI interaction not supported by generic script' });
11
+ const toggle = document.querySelector('#autonomy-mode-toggle-switch');
12
+ if (!toggle) {
13
+ return JSON.stringify({ success: false, error: 'Autonomy toggle not found' });
14
+ }
15
+
16
+ const isAutopilot = toggle.checked;
17
+
18
+ const wantAutopilot = mode.toLowerCase() === 'autopilot';
19
+ const wantManual = mode.toLowerCase() === 'manual';
20
+
21
+ if (wantAutopilot && !isAutopilot) {
22
+ toggle.click();
23
+ return JSON.stringify({ success: true });
24
+ } else if (wantManual && isAutopilot) {
25
+ toggle.click();
26
+ return JSON.stringify({ success: true });
27
+ }
28
+
29
+ // Already in the right mode
30
+ return JSON.stringify({ success: true });
12
31
  } catch (e) {
13
32
  return JSON.stringify({ success: false, error: e.message });
14
33
  }
@@ -1,14 +1,46 @@
1
1
  /**
2
- * Generic fallback set_model
3
- * ${ MODEL }
2
+ * Kirowebview_set_model
3
+ * 파라미터: ${ MODEL }
4
4
  */
5
- (() => {
5
+ (async () => {
6
6
  try {
7
7
  const want = ${ MODEL } || '';
8
- const norm = (t) => t.toLowerCase().trim();
8
+ const norm = (t) => (t || '').toLowerCase().trim();
9
9
 
10
- // Very basic click attempt
11
- return JSON.stringify({ success: false, error: 'Model selection requires UI interaction not supported by generic script' });
10
+ if (!want) return JSON.stringify({ success: false, error: 'No model specified' });
11
+
12
+ const trigger = document.querySelector('.kiro-dropdown-trigger');
13
+ if (!trigger) return JSON.stringify({ success: false, error: 'Trigger not found' });
14
+
15
+ const current = norm(trigger.querySelector('.kiro-dropdown-selected-text')?.textContent);
16
+ if (current === norm(want) || current.includes(norm(want))) {
17
+ return JSON.stringify({ success: true, already: true });
18
+ }
19
+
20
+ const wasExpanded = trigger.getAttribute('aria-expanded') === 'true';
21
+ if (!wasExpanded) {
22
+ trigger.click();
23
+ await new Promise(r => setTimeout(r, 150));
24
+ }
25
+
26
+ const items = document.querySelectorAll('.kiro-dropdown-item, [role="menuitem"], [role="option"]');
27
+ let found = null;
28
+ for (const item of items) {
29
+ const txt = norm(item.textContent);
30
+ if (txt === norm(want) || txt.includes(norm(want)) || norm(want).includes(txt)) {
31
+ found = item;
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (found) {
37
+ found.click();
38
+ return JSON.stringify({ success: true });
39
+ }
40
+
41
+ if (!wasExpanded) trigger.click(); // Close if we opened but didn't find
42
+
43
+ return JSON.stringify({ success: false, error: 'Model not found in list' });
12
44
  } catch (e) {
13
45
  return JSON.stringify({ success: false, error: e.message });
14
46
  }
@@ -137,7 +137,7 @@
137
137
  status = 'waiting_approval';
138
138
  activeModal = {
139
139
  message: normalize(cascade.querySelector('button')?.closest('[class*="terminal"],[class*="shadow-step"],.monaco-dialog-box,[role="dialog"]')?.innerText || 'Approval required'),
140
- actions: approvalActions,
140
+ buttons: approvalActions,
141
141
  };
142
142
  } else {
143
143
  const stopButton = Array.from(cascade.querySelectorAll('button, [role="button"]')).find(el => {
@@ -165,7 +165,7 @@ export class DaemonCliManager {
165
165
  }
166
166
  }
167
167
 
168
- try { addCliHistory({ cliType: normalizedType, dir: resolvedDir, cliArgs }); } catch (e) { LOG.warn('CLI', `ACP history save failed: ${(e as Error)?.message}`); }
168
+ try { addCliHistory({ category: 'acp', cliType: normalizedType, dir: resolvedDir, workspace: resolvedDir, cliArgs, model: initialModel }); } catch (e) { LOG.warn('CLI', `ACP history save failed: ${(e as Error)?.message}`); }
169
169
  this.deps.onStatusChange();
170
170
  return;
171
171
  }
@@ -261,7 +261,7 @@ export class DaemonCliManager {
261
261
  console.log(chalk.green(` ✓ CLI started: ${cliInfo.displayName} v${cliInfo.version || 'unknown'} in ${resolvedDir}`));
262
262
  }
263
263
 
264
- try { addCliHistory({ cliType, dir: resolvedDir, cliArgs }); } catch (e) { LOG.warn('CLI', `CLI history save failed: ${(e as Error)?.message}`); }
264
+ try { addCliHistory({ category: 'cli', cliType, dir: resolvedDir, workspace: resolvedDir, cliArgs, model: initialModel }); } catch (e) { LOG.warn('CLI', `CLI history save failed: ${(e as Error)?.message}`); }
265
265
 
266
266
  this.deps.onStatusChange();
267
267
  }
@@ -20,6 +20,7 @@ import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
20
20
  import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
21
21
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
22
22
  import { appendWorkspaceActivity } from '../config/workspace-activity.js';
23
+ import { addCliHistory } from '../config/config.js';
23
24
  import { detectIDEs } from '../detection/ide-detector.js';
24
25
  import { LOG } from '../logging/logger.js';
25
26
  import { logCommand } from '../logging/command-log.js';
@@ -207,6 +208,17 @@ export class DaemonCommandRouter {
207
208
  };
208
209
  LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
209
210
  const result = await launchWithCdp(launchArgs);
211
+ if (result.success && (result.ideId || ideKey)) {
212
+ try {
213
+ addCliHistory({
214
+ category: 'ide',
215
+ cliType: result.ideId || ideKey,
216
+ dir: resolvedWorkspace || '',
217
+ workspace: resolvedWorkspace || '',
218
+ newWindow: args?.newWindow === true,
219
+ });
220
+ } catch { /* ignore history failure */ }
221
+ }
210
222
 
211
223
  if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
212
224
  const logFn = this.deps.getCdpLogFn
@@ -87,9 +87,13 @@ export interface ADHDevConfig {
87
87
  }
88
88
 
89
89
  export interface CliHistoryEntry {
90
+ category?: 'ide' | 'cli' | 'acp';
90
91
  cliType: string;
91
92
  dir: string;
92
93
  cliArgs?: string[];
94
+ workspace?: string;
95
+ newWindow?: boolean;
96
+ model?: string;
93
97
  timestamp: number;
94
98
  label?: string;
95
99
  }
@@ -255,24 +259,43 @@ export function generateConnectionToken(): string {
255
259
  return token;
256
260
  }
257
261
  /**
258
- * Add CLI launch to history (max 20, dedup by cliType+dir+args)
262
+ * Add launch to history (max 20, dedup by category+type+dir+args+workspace+model)
259
263
  */
260
264
  export function addCliHistory(entry: Omit<CliHistoryEntry, 'timestamp'>): void {
261
265
  const config = loadConfig();
262
266
  const history = config.cliHistory || [];
263
267
  const argsKey = (entry.cliArgs || []).join(' ');
268
+ const category = entry.category || 'cli';
269
+ const workspaceKey = entry.workspace || '';
270
+ const modelKey = entry.model || '';
264
271
 
265
- // Remove duplicate (same cliType + dir + args)
272
+ // Remove duplicate (same category + type + dir + args + workspace + model)
266
273
  const filtered = history.filter(h => {
267
274
  const hArgsKey = (h.cliArgs || []).join(' ');
268
- return !(h.cliType === entry.cliType && h.dir === entry.dir && hArgsKey === argsKey);
275
+ return !(
276
+ (h.category || 'cli') === category &&
277
+ h.cliType === entry.cliType &&
278
+ h.dir === entry.dir &&
279
+ hArgsKey === argsKey &&
280
+ (h.workspace || '') === workspaceKey &&
281
+ (h.model || '') === modelKey
282
+ );
269
283
  });
270
284
 
271
285
  // Add to front
272
286
  filtered.unshift({
273
287
  ...entry,
288
+ category,
274
289
  timestamp: Date.now(),
275
- label: entry.label || `${entry.cliType} · ${entry.dir.split('/').filter(Boolean).pop() || 'root'}${argsKey ? ` (${argsKey})` : ''}`,
290
+ label: entry.label || (() => {
291
+ const base = `${entry.cliType} · ${entry.dir.split('/').filter(Boolean).pop() || 'root'}`;
292
+ const suffix: string[] = [];
293
+ if (entry.workspace && entry.workspace !== entry.dir) suffix.push(entry.workspace.split('/').filter(Boolean).pop() || entry.workspace);
294
+ if (entry.model) suffix.push(`model=${entry.model}`);
295
+ if (argsKey) suffix.push(argsKey);
296
+ if (entry.newWindow) suffix.push('new window');
297
+ return suffix.length > 0 ? `${base} (${suffix.join(' · ')})` : base;
298
+ })(),
276
299
  });
277
300
 
278
301
  // Keep max 20