ai-agent-session-center 2.10.1 → 2.10.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.
@@ -10,9 +10,9 @@ function str(val: unknown): string {
10
10
  if (Array.isArray(val)) return String(val[0] ?? '');
11
11
  return val != null ? String(val) : '';
12
12
  }
13
- import { findClaudeProcess, killSession, archiveSession, setSessionTitle, setSessionLabel, setSessionPinned, setSessionMuted, setSessionAlerted, setSessionAccentColor, setSessionCharacterModel, setSummary, getSession, getAllSessions, detectSessionSource, createTerminalSession, findActiveSessionByConfig, deleteSessionFromMemory, resumeSession, reconnectSessionTerminal, reconnectOpsTerminal, registerSessionAlias } from './sessionStore.js';
13
+ import { findClaudeProcess, killSession, archiveSession, setSessionTitle, setSessionLabel, setSessionPinned, setSessionMuted, setSessionAlerted, setSessionAccentColor, setSessionCharacterModel, setSummary, getSession, getAllSessions, detectSessionSource, createTerminalSession, findActiveSessionByConfig, deleteSessionFromMemory, clearAllSessions, resumeSession, reconnectSessionTerminal, reconnectOpsTerminal, registerSessionAlias } from './sessionStore.js';
14
14
  import { config as serverConfig } from './serverConfig.js';
15
- import { createTerminal, closeTerminal, getTerminals, listSshKeys, listTmuxSessions, writeToTerminal, writeWhenReady, attachToTmuxPane, consumePendingLink } from './sshManager.js';
15
+ import { createTerminal, closeTerminal, getTerminals, listSshKeys, listTmuxSessions, writeToTerminal, writeWhenReady, attachToTmuxPane, consumePendingLink, prefillTerminalOutput } from './sshManager.js';
16
16
  import { getTeam, readTeamConfig } from './teamManager.js';
17
17
  import { getStats as getHookStats, resetStats as resetHookStats } from './hookStats.js';
18
18
  import * as db from './db.js';
@@ -337,6 +337,26 @@ router.post('/hooks/uninstall', (_req: Request, res: Response) => {
337
337
 
338
338
  // ---- Session Control Endpoints ----
339
339
 
340
+ // Clear all sessions — kill terminals and remove from memory.
341
+ // MUST be registered before parameterized /sessions/:id routes to avoid
342
+ // Express matching "clear-all" as a session ID.
343
+ router.post('/sessions/clear-all', async (_req: Request, res: Response) => {
344
+ try {
345
+ const { removed, savedOutputs } = clearAllSessions();
346
+ // Broadcast clearBrowserDb so browsers clear their IndexedDB mirror
347
+ try {
348
+ const { broadcast } = await import('./wsManager.js');
349
+ broadcast({ type: WS_TYPES.CLEAR_BROWSER_DB });
350
+ } catch { /* ignore */ }
351
+ log.info('api', `Cleared all sessions: ${removed} removed, ${savedOutputs.length} output buffers saved`);
352
+ res.json({ ok: true, removed, savedOutputs });
353
+ } catch (err: unknown) {
354
+ const msg = err instanceof Error ? err.message : String(err);
355
+ log.error('api', `Clear all sessions failed: ${msg}`);
356
+ res.status(500).json({ ok: false, error: msg });
357
+ }
358
+ });
359
+
340
360
  // Resume a disconnected SSH session — tries `claude --resume <id>` first,
341
361
  // falls back to `claude --continue` if the conversation wasn't persisted.
342
362
  router.post('/sessions/:id/resume', async (req: Request, res: Response) => {
@@ -522,6 +542,66 @@ router.post('/sessions/:id/reconnect-terminal', async (req: Request, res: Respon
522
542
  }
523
543
  });
524
544
 
545
+ // Fork a Claude Code session: create a new terminal running
546
+ // `claude --continue <sessionId> --fork-session` in the same working directory.
547
+ router.post('/sessions/:id/fork', async (req: Request, res: Response) => {
548
+ const sessionId = str(req.params.id);
549
+ const session = getSession(sessionId);
550
+ if (!session) { res.status(404).json({ error: 'Session not found' }); return; }
551
+
552
+ const forkCmd = `claude --continue --fork-session`;
553
+
554
+ try {
555
+ const cfg = session.sshConfig;
556
+
557
+ if (cfg && cfg.username) {
558
+ // SSH session: create new terminal with same SSH config
559
+ const newConfig: TerminalConfig = { ...cfg, workingDir: cfg.workingDir || '~', command: '' };
560
+ const newTerminalId = await createTerminal(newConfig, null);
561
+ consumePendingLink(newConfig.workingDir || session.projectPath || '');
562
+
563
+ await createTerminalSession(newTerminalId, {
564
+ ...newConfig,
565
+ command: forkCmd,
566
+ sessionTitle: `Fork of ${session.title || session.projectName}`,
567
+ });
568
+
569
+ const isRemote = cfg.host && cfg.host !== 'localhost' && cfg.host !== '127.0.0.1';
570
+ let prefix = '';
571
+ if (isRemote) {
572
+ prefix += `export AGENT_MANAGER_TERMINAL_ID='${newTerminalId}' && `;
573
+ if (cfg.workingDir) prefix += `cd '${cfg.workingDir}' && `;
574
+ }
575
+ writeWhenReady(newTerminalId, `${prefix}${forkCmd}\r`);
576
+
577
+ res.json({ ok: true, terminalId: newTerminalId });
578
+ } else {
579
+ // Non-SSH (hook-detected / local) session
580
+ const newConfig: TerminalConfig = {
581
+ host: 'localhost',
582
+ workingDir: session.projectPath || '~',
583
+ command: '',
584
+ };
585
+ const newTerminalId = await createTerminal(newConfig, null);
586
+ consumePendingLink(newConfig.workingDir || session.projectPath || '');
587
+
588
+ await createTerminalSession(newTerminalId, {
589
+ ...newConfig,
590
+ command: forkCmd,
591
+ sessionTitle: `Fork of ${session.title || session.projectName}`,
592
+ });
593
+
594
+ writeWhenReady(newTerminalId, `${forkCmd}\r`);
595
+
596
+ res.json({ ok: true, terminalId: newTerminalId });
597
+ }
598
+ } catch (err: unknown) {
599
+ const msg = err instanceof Error ? err.message : String(err);
600
+ log.error('api', `Fork session failed: ${msg}`);
601
+ res.status(500).json({ error: 'Failed to fork session' });
602
+ }
603
+ });
604
+
525
605
  // Reconnect (or create) the ops terminal for a session.
526
606
  // Creates a new blank shell in the session's project directory.
527
607
  router.post('/sessions/:id/reconnect-ops-terminal', async (req: Request, res: Response) => {
@@ -961,6 +1041,23 @@ router.post('/queue-images', (req: Request, res: Response) => {
961
1041
  res.json({ ok: true, paths });
962
1042
  });
963
1043
 
1044
+ // Prefill a terminal's output ring buffer with saved data (base64).
1045
+ // Used during workspace import to restore previous terminal scrollback.
1046
+ router.post('/terminals/:id/prefill-output', (req: Request, res: Response) => {
1047
+ const terminalId = str(req.params.id);
1048
+ const { data } = req.body || {};
1049
+ if (!data || typeof data !== 'string') {
1050
+ res.status(400).json({ error: 'Missing or invalid "data" field (base64)' });
1051
+ return;
1052
+ }
1053
+ const ok = prefillTerminalOutput(terminalId, data);
1054
+ if (!ok) {
1055
+ res.status(404).json({ error: 'Terminal not found' });
1056
+ return;
1057
+ }
1058
+ res.json({ ok: true });
1059
+ });
1060
+
964
1061
  router.post('/terminals/:id/write', (req: Request, res: Response) => {
965
1062
  const terminalId = str(req.params.id);
966
1063
  const { data } = req.body || {};
package/server/config.ts CHANGED
@@ -162,3 +162,19 @@ export function reconstructPermissionFlags(baseCmd: string, permissionMode?: str
162
162
  // "default", "plan", or unknown → no flag needed
163
163
  return baseCmd;
164
164
  }
165
+
166
+ /**
167
+ * Append `-n "title"` to a Claude command for session naming.
168
+ * Only applies to Claude CLI commands (starts with "claude").
169
+ * Skips if the command already contains a `-n` flag.
170
+ */
171
+ export function appendSessionName(command: string, sessionTitle?: string | null): string {
172
+ if (!sessionTitle) return command;
173
+ // Only Claude CLI supports -n flag
174
+ if (!command.startsWith('claude')) return command;
175
+ // Don't double-add if already present
176
+ if (/ -n[ =]/.test(command) || / --name[ =]/.test(command)) return command;
177
+ // Escape double quotes in the title
178
+ const escaped = sessionTitle.replace(/"/g, '\\"');
179
+ return `${command} -n "${escaped}"`;
180
+ }
@@ -21,7 +21,7 @@ import {
21
21
  // Sub-module imports
22
22
  import { matchSession, detectHookSource } from './sessionMatcher.js';
23
23
  import { startApprovalTimer, clearApprovalTimer, hasChildProcesses } from './approvalDetector.js';
24
- import { closeTerminal, registerTerminalExitCallback } from './sshManager.js';
24
+ import { closeTerminal, registerTerminalExitCallback, getTerminalOutputBuffer } from './sshManager.js';
25
25
  import {
26
26
  findPendingSubagentMatch, handleTeamMemberEnd, addPendingSubagent,
27
27
  linkByParentSessionId,
@@ -909,6 +909,10 @@ export function findActiveSessionByConfig(config: TerminalConfig): Session | nul
909
909
  if (session.projectPath !== resolvedWorkDir) continue;
910
910
  if ((session.sshHost || 'localhost') !== resolvedHost) continue;
911
911
  if ((session.sshCommand || 'claude') !== resolvedCommand) continue;
912
+ // When a session title is provided in the config, require it to match.
913
+ // This prevents collapsing distinct sessions that share the same workDir
914
+ // and command but have different titles (e.g. "MD Task" and "MD").
915
+ if (config.sessionTitle && session.title !== config.sessionTitle) continue;
912
916
  return { ...session };
913
917
  }
914
918
  return null;
@@ -1062,6 +1066,53 @@ export function deleteSessionFromMemory(sessionId: string): boolean {
1062
1066
  return true;
1063
1067
  }
1064
1068
 
1069
+ export interface SavedTerminalOutput {
1070
+ /** Stable key: sessionTitle + '\0' + workingDir (used to match after reimport) */
1071
+ key: string;
1072
+ /** Base64-encoded terminal output ring buffer */
1073
+ data: string;
1074
+ }
1075
+
1076
+ /**
1077
+ * Kill and remove ALL sessions from memory.
1078
+ * Used during workspace import to clear the slate before loading a new snapshot.
1079
+ * Captures terminal output buffers before killing so they can be replayed in new sessions.
1080
+ * Returns the number of sessions removed and saved output buffers.
1081
+ */
1082
+ export function clearAllSessions(): { removed: number; savedOutputs: SavedTerminalOutput[] } {
1083
+ const ids = [...sessions.keys()];
1084
+ let removed = 0;
1085
+ const savedOutputs: SavedTerminalOutput[] = [];
1086
+
1087
+ for (const id of ids) {
1088
+ const session = sessions.get(id);
1089
+ if (!session) continue;
1090
+
1091
+ // Capture terminal output buffer BEFORE killing the terminal
1092
+ if (session.terminalId) {
1093
+ const buf = getTerminalOutputBuffer(session.terminalId);
1094
+ if (buf) {
1095
+ const key = (session.title || '') + '\0' + (session.projectPath || '');
1096
+ savedOutputs.push({ key, data: buf });
1097
+ }
1098
+ closeTerminal(session.terminalId);
1099
+ }
1100
+ if (session.opsTerminalId) {
1101
+ closeTerminal(session.opsTerminalId);
1102
+ }
1103
+ // Release PID cache
1104
+ if (session.cachedPid) {
1105
+ pidToSession.delete(session.cachedPid);
1106
+ }
1107
+ sessions.delete(id);
1108
+ removed++;
1109
+ }
1110
+ // Clear all aliases
1111
+ sessionAliases.clear();
1112
+ invalidateSessionsCache();
1113
+ return { removed, savedOutputs };
1114
+ }
1115
+
1065
1116
  export function setSessionTitle(sessionId: string, title: string): Session | null {
1066
1117
  const session = sessions.get(sessionId);
1067
1118
  if (session) { session.title = title; invalidateSessionsCache(); dbUpdateTitle(sessionId, title); }
@@ -9,6 +9,7 @@ import { readdirSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { homedir, networkInterfaces, hostname as osHostname } from 'os';
11
11
  import log from './logger.js';
12
+ import { appendSessionName } from './config.js';
12
13
  import type { Terminal, TerminalConfig, TerminalInfo, TmuxSessionInfo, SshKeyInfo } from '../src/types/terminal.js';
13
14
  import type { PendingLink } from '../src/types/session.js';
14
15
  import type WebSocket from 'ws';
@@ -151,6 +152,17 @@ export function listSshKeys(): SshKeyInfo[] {
151
152
  }
152
153
  }
153
154
 
155
+ // Auto-generate session name: projectName #N (sequence counter per project)
156
+ const projectNameCounters = new Map<string, number>();
157
+ function autoSessionName(workDir: string): string {
158
+ const projectName = workDir === homedir()
159
+ ? 'Home'
160
+ : workDir.split('/').filter(Boolean).pop() || 'Session';
161
+ const counter = (projectNameCounters.get(projectName) || 0) + 1;
162
+ projectNameCounters.set(projectName, counter);
163
+ return `${projectName} #${counter}`;
164
+ }
165
+
154
166
  // Active terminals: terminalId -> Terminal
155
167
  const terminals = new Map<string, Terminal>();
156
168
 
@@ -273,7 +285,10 @@ export function createTerminal(config: TerminalConfig, wsClient: WebSocket | nul
273
285
 
274
286
  const terminalId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
275
287
  const workDir = resolveWorkDir(config.workingDir);
276
- const command = config.command || 'claude';
288
+ const baseCommand = config.command || 'claude';
289
+ // Append -n "title" to Claude commands for session naming
290
+ const sessionName = config.sessionTitle || autoSessionName(workDir);
291
+ const command = appendSessionName(baseCommand, sessionName);
277
292
  const skipAutoLaunch = config.command === '';
278
293
  const local = isLocal(config.host);
279
294
 
@@ -581,10 +596,18 @@ export function attachToTmuxPane(tmuxPaneId: string, wsClient: WebSocket | null)
581
596
  });
582
597
  }
583
598
 
599
+ /**
600
+ * Strip terminal response sequences that should never reach PTY stdin.
601
+ * Focus events (\x1b[I / \x1b[O) and Device Attributes responses
602
+ * (\x1b[?...c / \x1b[>...c) are emitted by xterm.js but are not user input.
603
+ */
604
+ const TERMINAL_RESPONSE_RE = /\x1b\[I|\x1b\[O|\x1b\[\?[\d;]*c|\x1b\[>[\d;]*c/g;
605
+
584
606
  export function writeToTerminal(terminalId: string, data: string): void {
585
607
  const term = terminals.get(terminalId);
586
608
  if (term && term.pty) {
587
- term.pty.write(data);
609
+ const cleaned = data.replace(TERMINAL_RESPONSE_RE, '');
610
+ if (cleaned) term.pty.write(cleaned);
588
611
  }
589
612
  }
590
613
 
@@ -727,6 +750,29 @@ export function setWsClient(terminalId: string, wsClient: WebSocket | null): boo
727
750
  return true;
728
751
  }
729
752
 
753
+ /** Get the raw output ring buffer for a terminal (base64-encoded). */
754
+ export function getTerminalOutputBuffer(terminalId: string): string | null {
755
+ const term = terminals.get(terminalId);
756
+ if (!term || term.outputBuffer.length === 0) return null;
757
+ return term.outputBuffer.toString('base64');
758
+ }
759
+
760
+ /**
761
+ * Prepend saved output (base64) into a terminal's ring buffer.
762
+ * Used during workspace import to restore previous terminal scrollback.
763
+ * The saved output is prepended before any new PTY output.
764
+ */
765
+ export function prefillTerminalOutput(terminalId: string, base64Data: string): boolean {
766
+ const term = terminals.get(terminalId);
767
+ if (!term) return false;
768
+ const saved = Buffer.from(base64Data, 'base64');
769
+ term.outputBuffer = Buffer.concat([saved, term.outputBuffer]);
770
+ if (term.outputBuffer.length > OUTPUT_BUFFER_MAX) {
771
+ term.outputBuffer = term.outputBuffer.slice(term.outputBuffer.length - OUTPUT_BUFFER_MAX);
772
+ }
773
+ return true;
774
+ }
775
+
730
776
  export function getTerminals(): TerminalInfo[] {
731
777
  const result: TerminalInfo[] = [];
732
778
  for (const [terminalId, term] of terminals) {
@@ -1 +0,0 @@
1
- ._container_1hp2u_6{display:flex;flex-direction:column;height:100%;overflow:hidden}._filterBar_1hp2u_15{display:flex;align-items:center;gap:10px;padding:14px 24px;border-bottom:1px solid var(--border-subtle);flex-shrink:0;flex-wrap:wrap}._filterBar_1hp2u_15 select{background:var(--bg-primary, #0a0a1a);border:1px solid var(--border-subtle);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:var(--font-mono);padding:6px 10px;outline:none;cursor:pointer;min-width:100px}._filterBar_1hp2u_15 select:focus{border-color:var(--accent-cyan)}._toggleLabel_1hp2u_42{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-secondary);font-family:var(--font-mono);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap}._toggleLabel_1hp2u_42 input[type=checkbox]{accent-color:var(--accent-cyan);cursor:pointer}._taskList_1hp2u_61{flex:1;overflow-y:auto;padding:12px 24px}._groupHeader_1hp2u_69{display:flex;align-items:center;gap:8px;padding:8px 0 6px;cursor:pointer;-webkit-user-select:none;user-select:none}._groupChevron_1hp2u_78{width:10px;height:10px;flex-shrink:0;transition:transform .15s ease;color:var(--text-dim)}._groupChevron_1hp2u_78._collapsed_1hp2u_86{transform:rotate(-90deg)}._groupLabel_1hp2u_90{font-size:11px;letter-spacing:1.5px;color:var(--text-dim);text-transform:uppercase;font-family:var(--font-mono);flex:1}._groupCount_1hp2u_99{font-size:10px;color:var(--text-dim);font-family:var(--font-mono)}._taskCard_1hp2u_107{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border:1px solid var(--border-subtle);border-radius:6px;margin-bottom:6px;background:transparent;transition:background .15s ease,border-color .15s ease,box-shadow .15s ease}._taskCard_1hp2u_107:hover{background:var(--bg-subtle);border-color:#ffffff1a}._completed_1hp2u_124{opacity:.5}._completed_1hp2u_124 ._taskTitle_1hp2u_128{text-decoration:line-through;color:var(--text-dim)}._overdue_1hp2u_133{border-color:var(--accent-red);box-shadow:0 0 8px color-mix(in srgb,var(--accent-red) 30%,transparent)}._dueToday_1hp2u_138{border-color:var(--accent-yellow);box-shadow:0 0 8px color-mix(in srgb,var(--accent-yellow) 20%,transparent)}._checkbox_1hp2u_145{width:18px;height:18px;flex-shrink:0;margin-top:1px;accent-color:var(--accent-cyan);cursor:pointer}._checkbox_1hp2u_145:checked{filter:drop-shadow(0 0 4px var(--accent-cyan))}._taskContent_1hp2u_160{flex:1;min-width:0}._taskHeader_1hp2u_165{display:flex;align-items:center;gap:8px;margin-bottom:2px}._taskTitle_1hp2u_128{font-size:13px;color:var(--text-primary);font-family:var(--font-mono);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;line-height:1.4}._taskTitle_1hp2u_128:hover{color:var(--accent-cyan)}._taskTitleInput_1hp2u_189{width:100%;background:var(--bg-subtle);border:1px solid var(--accent-cyan);border-radius:3px;color:var(--text-primary);font-size:13px;font-family:var(--font-mono);padding:1px 6px;outline:none;line-height:1.4}._priorityBadge_1hp2u_204{font-size:9px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;padding:1px 6px;border-radius:3px;flex-shrink:0;line-height:1.6;transition:filter .15s ease,transform .15s ease}._priorityBadge_1hp2u_204:hover{filter:brightness(1.3);transform:scale(1.05)}._priorityUrgent_1hp2u_221{background:color-mix(in srgb,var(--accent-red) 20%,transparent);color:var(--accent-red);border:1px solid color-mix(in srgb,var(--accent-red) 40%,transparent)}._priorityHigh_1hp2u_227{background:color-mix(in srgb,var(--accent-orange) 20%,transparent);color:var(--accent-orange);border:1px solid color-mix(in srgb,var(--accent-orange) 40%,transparent)}._priorityMedium_1hp2u_233{background:color-mix(in srgb,var(--accent-yellow) 20%,transparent);color:var(--accent-yellow);border:1px solid color-mix(in srgb,var(--accent-yellow) 40%,transparent)}._priorityLow_1hp2u_239{background:color-mix(in srgb,var(--accent-green) 20%,transparent);color:var(--accent-green);border:1px solid color-mix(in srgb,var(--accent-green) 40%,transparent)}._taskMeta_1hp2u_247{display:flex;align-items:center;gap:8px;margin-top:4px;flex-wrap:wrap}._dueDate_1hp2u_255{font-size:10px;font-family:var(--font-mono);color:var(--text-dim)}._dueDateOverdue_1hp2u_261{color:var(--accent-red)}._dueDateToday_1hp2u_265{color:var(--accent-yellow)}._tag_1hp2u_269{font-size:9px;color:var(--accent-cyan);background:color-mix(in srgb,var(--accent-cyan) 10%,transparent);border:1px solid color-mix(in srgb,var(--accent-cyan) 25%,transparent);padding:0 5px;border-radius:3px;line-height:1.7}._tag_1hp2u_269:hover{background:color-mix(in srgb,var(--accent-cyan) 20%,transparent);border-color:var(--accent-cyan)}._tagInput_1hp2u_284{font-size:10px;font-family:var(--font-mono);color:var(--text-primary);background:var(--bg-subtle);border:1px solid var(--accent-cyan);border-radius:3px;padding:1px 6px;outline:none;min-width:120px;line-height:1.5}._addTagBtn_1hp2u_297{font-size:10px;color:var(--text-dim);background:none;border:1px dashed color-mix(in srgb,var(--text-dim) 40%,transparent);border-radius:3px;padding:0 5px;cursor:pointer;font-family:var(--font-mono);line-height:1.6;transition:all .15s ease}._addTagBtn_1hp2u_297:hover{color:var(--accent-cyan);border-color:var(--accent-cyan)}._description_1hp2u_317{margin-top:6px;font-size:11px;color:var(--text-secondary);font-family:var(--font-mono);line-height:1.5;white-space:pre-wrap;word-break:break-word}._descriptionToggle_1hp2u_327{font-size:10px;color:var(--text-dim);cursor:pointer;background:none;border:none;padding:0;font-family:var(--font-mono);margin-top:4px}._descriptionToggle_1hp2u_327:hover{color:var(--accent-cyan)}._deleteBtn_1hp2u_344{flex-shrink:0;background:none;border:none;color:var(--text-dim);cursor:pointer;padding:4px;border-radius:3px;transition:color .15s ease,background .15s ease;display:flex;align-items:center;justify-content:center}._deleteBtn_1hp2u_344:hover{color:var(--accent-red);background:color-mix(in srgb,var(--accent-red) 15%,transparent)}._addForm_1hp2u_365{display:flex;align-items:flex-end;gap:8px;padding:12px 24px 16px;border-top:1px solid var(--border-subtle);flex-shrink:0;flex-wrap:wrap}._addForm_1hp2u_365 input[type=text],._addForm_1hp2u_365 input[type=date]{background:var(--bg-primary, #0a0a1a);border:1px solid var(--border-subtle);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:var(--font-mono);padding:7px 10px;outline:none}._addForm_1hp2u_365 input[type=text]:focus,._addForm_1hp2u_365 input[type=date]:focus{border-color:var(--accent-cyan)}._addForm_1hp2u_365 select{background:var(--bg-primary, #0a0a1a);border:1px solid var(--border-subtle);border-radius:6px;color:var(--text-primary);font-size:12px;font-family:var(--font-mono);padding:7px 10px;outline:none;cursor:pointer}._addForm_1hp2u_365 select:focus{border-color:var(--accent-cyan)}._titleInput_1hp2u_408{flex:1;min-width:180px}._tagsInput_1hp2u_413{min-width:120px;width:140px}._addBtn_1hp2u_418{background:color-mix(in srgb,var(--accent-cyan) 15%,transparent);border:1px solid var(--accent-cyan);border-radius:6px;color:var(--accent-cyan);font-size:11px;font-family:var(--font-mono);font-weight:600;letter-spacing:1px;padding:7px 16px;cursor:pointer;transition:background .15s ease;white-space:nowrap}._addBtn_1hp2u_418:hover:not(:disabled){background:color-mix(in srgb,var(--accent-cyan) 25%,transparent)}._addBtn_1hp2u_418:disabled{opacity:.4;cursor:not-allowed}._emptyState_1hp2u_444{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 24px;color:var(--text-dim);font-size:13px;font-family:var(--font-mono);text-align:center;gap:8px}._emptyState_1hp2u_444 span{font-size:11px;color:var(--text-dim);opacity:.6}._loading_1hp2u_465{display:flex;align-items:center;justify-content:center;padding:60px 24px;color:var(--text-dim);font-size:12px;font-family:var(--font-mono);letter-spacing:1px}._stats_1hp2u_478{padding:6px 24px;font-size:11px;color:var(--text-secondary);font-family:var(--font-mono);display:flex;gap:6px;flex-shrink:0}._statsSep_1hp2u_488{color:var(--text-dim)}._confirmOverlay_1hp2u_494{display:flex;align-items:center;gap:6px;font-size:10px;font-family:var(--font-mono);color:var(--accent-red)}._confirmBtn_1hp2u_503{background:color-mix(in srgb,var(--accent-red) 15%,transparent);border:1px solid var(--accent-red);border-radius:3px;color:var(--accent-red);font-size:10px;font-family:var(--font-mono);padding:2px 8px;cursor:pointer}._confirmBtn_1hp2u_503:hover{background:color-mix(in srgb,var(--accent-red) 30%,transparent)}._cancelBtn_1hp2u_518{background:none;border:1px solid var(--border-subtle);border-radius:3px;color:var(--text-secondary);font-size:10px;font-family:var(--font-mono);padding:2px 8px;cursor:pointer}._cancelBtn_1hp2u_518:hover{border-color:var(--text-dim)}
@@ -1 +0,0 @@
1
- import{y as f,h as i,k as e,o as F}from"./index-DMLiHQ-O.js";const $="_container_1hp2u_6",U="_filterBar_1hp2u_15",Y="_toggleLabel_1hp2u_42",G="_taskList_1hp2u_61",V="_groupHeader_1hp2u_69",W="_groupChevron_1hp2u_78",q="_collapsed_1hp2u_86",K="_groupLabel_1hp2u_90",z="_groupCount_1hp2u_99",J="_taskCard_1hp2u_107",Q="_completed_1hp2u_124",X="_taskTitle_1hp2u_128",Z="_overdue_1hp2u_133",ee="_dueToday_1hp2u_138",te="_checkbox_1hp2u_145",se="_taskContent_1hp2u_160",ae="_taskHeader_1hp2u_165",ne="_taskTitleInput_1hp2u_189",oe="_priorityBadge_1hp2u_204",ie="_priorityUrgent_1hp2u_221",le="_priorityHigh_1hp2u_227",re="_priorityMedium_1hp2u_233",ce="_priorityLow_1hp2u_239",de="_taskMeta_1hp2u_247",ue="_dueDate_1hp2u_255",pe="_dueDateOverdue_1hp2u_261",he="_dueDateToday_1hp2u_265",ge="_tag_1hp2u_269",me="_tagInput_1hp2u_284",_e="_addTagBtn_1hp2u_297",fe="_description_1hp2u_317",ye="_descriptionToggle_1hp2u_327",xe="_deleteBtn_1hp2u_344",ve="_addForm_1hp2u_365",je="_titleInput_1hp2u_408",Te="_tagsInput_1hp2u_413",Ce="_addBtn_1hp2u_418",ke="_emptyState_1hp2u_444",De="_loading_1hp2u_465",be="_stats_1hp2u_478",we="_statsSep_1hp2u_488",Se="_confirmOverlay_1hp2u_494",Ne="_confirmBtn_1hp2u_503",Be="_cancelBtn_1hp2u_518",s={container:$,filterBar:U,toggleLabel:Y,taskList:G,groupHeader:V,groupChevron:W,collapsed:q,groupLabel:K,groupCount:z,taskCard:J,completed:Q,taskTitle:X,overdue:Z,dueToday:ee,checkbox:te,taskContent:se,taskHeader:ae,taskTitleInput:ne,priorityBadge:oe,priorityUrgent:ie,priorityHigh:le,priorityMedium:re,priorityLow:ce,taskMeta:de,dueDate:ue,dueDateOverdue:pe,dueDateToday:he,tag:ge,tagInput:me,addTagBtn:_e,description:fe,descriptionToggle:ye,deleteBtn:xe,addForm:ve,titleInput:je,tagsInput:Te,addBtn:Ce,emptyState:ke,loading:De,stats:be,statsSep:we,confirmOverlay:Se,confirmBtn:Ne,cancelBtn:Be},Ie=[{value:"all",label:"All priorities"},{value:"urgent",label:"Urgent"},{value:"high",label:"High"},{value:"medium",label:"Medium"},{value:"low",label:"Low"}],Le=[{value:"priority",label:"Priority"},{value:"dueDate",label:"Due date"},{value:"createdAt",label:"Created"}];function Oe(){const t=f(l=>l.filter),o=f(l=>l.setFilter),n=i.useCallback(l=>o({search:l}),[o]),c=i.useCallback(l=>o({priority:l.target.value}),[o]),d=i.useCallback(l=>o({sortBy:l.target.value}),[o]),p=i.useCallback(()=>o({showCompleted:!t.showCompleted}),[o,t.showCompleted]);return e.jsxs("div",{className:s.filterBar,children:[e.jsx(F,{value:t.search,onChange:n,placeholder:"Search tasks...",debounceMs:200}),e.jsx("select",{value:t.priority,onChange:c,children:Ie.map(l=>e.jsx("option",{value:l.value,children:l.label},l.value))}),e.jsx("select",{value:t.sortBy,onChange:d,children:Le.map(l=>e.jsx("option",{value:l.value,children:l.label},l.value))}),e.jsxs("label",{className:s.toggleLabel,children:[e.jsx("input",{type:"checkbox",checked:t.showCompleted,onChange:p}),"Show completed"]})]})}const Re={urgent:s.priorityUrgent,high:s.priorityHigh,medium:s.priorityMedium,low:s.priorityLow},B=["low","medium","high","urgent"];function Ee(t){if(!t)return null;const o=new Date;o.setHours(0,0,0,0);const n=new Date(t+"T00:00:00");return n<o?"overdue":n.getTime()===o.getTime()?"today":"future"}function He(t){return new Date(t+"T00:00:00").toLocaleDateString("en-US",{month:"short",day:"numeric"})}function L({task:t}){const o=f(a=>a.toggleTask),n=f(a=>a.updateTask),c=f(a=>a.deleteTask),[d,p]=i.useState(!1),[l,m]=i.useState(t.title),[_,x]=i.useState(!1),[u,j]=i.useState(""),[T,r]=i.useState(!1),[y,g]=i.useState(!1),C=i.useRef(null),b=i.useRef(null);i.useEffect(()=>{d&&(C.current?.focus(),C.current?.select())},[d]),i.useEffect(()=>{_&&b.current?.focus()},[_]),i.useEffect(()=>{d||m(t.title)},[t.title,d]);const k=i.useCallback(()=>{p(!1);const a=l.trim();a&&a!==t.title?n(t.id,{title:a}):m(t.title)},[l,t.id,t.title,n]),w=i.useCallback(()=>{o(t.id)},[o,t.id]),h=i.useCallback(()=>{c(t.id),g(!1)},[c,t.id]),v=i.useCallback(()=>{j(t.tags.join(", ")),x(!0)},[t.tags]),M=i.useCallback(()=>{const a=B.indexOf(t.priority),N=B[(a+1)%B.length];n(t.id,{priority:N})},[t.id,t.priority,n]),I=i.useCallback(()=>{x(!1);const a=u.split(",").map(P=>P.trim()).filter(Boolean),N=t.tags.join(","),A=a.join(",");N!==A&&n(t.id,{tags:a})},[u,t.id,t.tags,n]),D=Ee(t.dueDate),S=[s.taskCard];return t.completed&&S.push(s.completed),!t.completed&&D==="overdue"&&S.push(s.overdue),!t.completed&&D==="today"&&S.push(s.dueToday),e.jsxs("div",{className:S.join(" "),children:[e.jsx("input",{type:"checkbox",className:s.checkbox,checked:t.completed,onChange:w,"aria-label":`Mark "${t.title}" as ${t.completed?"incomplete":"complete"}`}),e.jsxs("div",{className:s.taskContent,children:[e.jsxs("div",{className:s.taskHeader,children:[d?e.jsx("input",{ref:C,className:s.taskTitleInput,value:l,onChange:a=>m(a.target.value),onBlur:k,onKeyDown:a=>{a.key==="Enter"&&(a.preventDefault(),k()),a.key==="Escape"&&(m(t.title),p(!1))},onClick:a=>a.stopPropagation()}):e.jsx("span",{className:s.taskTitle,onClick:()=>{m(t.title),p(!0)},title:"Click to edit",children:t.title}),e.jsx("span",{className:`${s.priorityBadge} ${Re[t.priority]}`,onClick:a=>{a.stopPropagation(),M()},title:"Click to change priority",style:{cursor:"pointer"},children:t.priority})]}),e.jsxs("div",{className:s.taskMeta,children:[t.dueDate&&e.jsxs("span",{className:`${s.dueDate} ${D==="overdue"?s.dueDateOverdue:""} ${D==="today"?s.dueDateToday:""}`,children:[D==="overdue"&&"Overdue: ",D==="today"&&"Today: ",He(t.dueDate)]}),_?e.jsx("input",{ref:b,className:s.tagInput,value:u,onChange:a=>j(a.target.value),onBlur:I,onKeyDown:a=>{a.key==="Enter"&&(a.preventDefault(),I()),a.key==="Escape"&&x(!1)},onClick:a=>a.stopPropagation(),placeholder:"tag1, tag2, ..."}):e.jsxs(e.Fragment,{children:[t.tags.map(a=>e.jsx("span",{className:s.tag,onClick:v,title:"Click to edit tags",style:{cursor:"pointer"},children:a},a)),e.jsx("button",{className:s.addTagBtn,onClick:v,title:"Add / edit tags",children:"+"})]})]}),t.description&&e.jsxs(e.Fragment,{children:[e.jsx("button",{className:s.descriptionToggle,onClick:()=>r(a=>!a),children:T?"- Hide details":"+ Show details"}),T&&e.jsx("div",{className:s.description,children:t.description})]})]}),y?e.jsxs("div",{className:s.confirmOverlay,children:[e.jsx("span",{children:"Delete?"}),e.jsx("button",{className:s.confirmBtn,onClick:h,children:"Yes"}),e.jsx("button",{className:s.cancelBtn,onClick:()=>g(!1),children:"No"})]}):e.jsx("button",{className:s.deleteBtn,onClick:()=>g(!0),"aria-label":`Delete "${t.title}"`,title:"Delete task",children:e.jsxs("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e.jsx("polyline",{points:"3 6 5 6 21 6"}),e.jsx("path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"})]})})]})}function Me(){const t=f(u=>u.createTask),[o,n]=i.useState(""),[c,d]=i.useState("medium"),[p,l]=i.useState(""),[m,_]=i.useState(""),x=i.useCallback(u=>{u.preventDefault();const j=o.trim();if(!j)return;const T=m.split(",").map(r=>r.trim()).filter(Boolean);t({title:j,priority:c,tags:T,dueDate:p||void 0}),n(""),d("medium"),l(""),_("")},[o,c,p,m,t]);return e.jsxs("form",{className:s.addForm,onSubmit:x,children:[e.jsx("input",{type:"text",className:s.titleInput,placeholder:"New task title...",value:o,onChange:u=>n(u.target.value),required:!0}),e.jsxs("select",{value:c,onChange:u=>d(u.target.value),children:[e.jsx("option",{value:"urgent",children:"Urgent"}),e.jsx("option",{value:"high",children:"High"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"low",children:"Low"})]}),e.jsx("input",{type:"date",value:p,onChange:u=>l(u.target.value),title:"Due date (optional)"}),e.jsx("input",{type:"text",className:s.tagsInput,placeholder:"Tags (comma-sep)",value:m,onChange:u=>_(u.target.value)}),e.jsx("button",{type:"submit",className:s.addBtn,disabled:!o.trim(),children:"ADD"})]})}const O=["urgent","high","medium","low"],R={urgent:0,high:1,medium:2,low:3},Ae={urgent:"Urgent",high:"High",medium:"Medium",low:"Low"};function Pe(t,o,n,c){if(n!=="all"&&t.priority!==n||c!=="all"&&!t.tags.includes(c))return!1;if(o){const d=o.toLowerCase(),p=t.title.toLowerCase().includes(d),l=(t.description??"").toLowerCase().includes(d);if(!p&&!l)return!1}return!0}function E(t,o){return[...t].sort((n,c)=>{if(o==="priority"){const d=R[n.priority],p=R[c.priority];return d!==p?d-p:new Date(c.createdAt).getTime()-new Date(n.createdAt).getTime()}return o==="dueDate"?!n.dueDate&&!c.dueDate?0:n.dueDate?c.dueDate?n.dueDate.localeCompare(c.dueDate):-1:1:new Date(c.createdAt).getTime()-new Date(n.createdAt).getTime()})}function H({label:t,count:o,collapsed:n,onToggle:c}){return e.jsxs("div",{className:s.groupHeader,onClick:c,children:[e.jsx("svg",{className:`${s.groupChevron} ${n?s.collapsed:""}`,width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round",children:e.jsx("polyline",{points:"6 9 12 15 18 9"})}),e.jsx("span",{className:s.groupLabel,children:t}),e.jsx("span",{className:s.groupCount,children:o})]})}function $e(){const t=f(r=>r.tasks),o=f(r=>r.loading),n=f(r=>r.filter),c=f(r=>r.fetchTasks),[d,p]=i.useState(new Set);i.useEffect(()=>{c()},[c]);const l=i.useCallback(r=>{p(y=>{const g=new Set(y);return g.has(r)?g.delete(r):g.add(r),g})},[]),{groups:m,completedTasks:_,totalIncomplete:x,totalCompleted:u}=i.useMemo(()=>{const y=[...t.values()].filter(h=>Pe(h,n.search,n.priority,n.tag)),g=y.filter(h=>!h.completed),C=y.filter(h=>h.completed),b=E(g,n.sortBy),k=new Map;for(const h of O)k.set(h,[]);for(const h of b){const v=k.get(h.priority);v&&v.push(h)}const w=[];for(const h of O){const v=k.get(h)??[];v.length>0&&w.push({id:h,label:Ae[h],tasks:v})}return{groups:w,completedTasks:E(C,n.sortBy),totalIncomplete:g.length,totalCompleted:C.length}},[t,n]);if(o)return e.jsx("div",{className:s.container,children:e.jsx("div",{className:s.loading,children:"Loading tasks..."})});const j=m.length===0&&_.length===0,T=t.size===0;return e.jsxs("div",{className:s.container,"data-testid":"agenda-view",children:[e.jsx(Oe,{}),e.jsxs("div",{className:s.stats,children:[e.jsxs("span",{children:[x," task",x!==1?"s":""]}),e.jsx("span",{className:s.statsSep,children:"|"}),e.jsxs("span",{children:[u," completed"]})]}),e.jsx("div",{className:s.taskList,children:T?e.jsxs("div",{className:s.emptyState,children:[e.jsx("div",{children:"No tasks yet"}),e.jsx("span",{children:"Add your first task below"})]}):j?e.jsxs("div",{className:s.emptyState,children:[e.jsx("div",{children:"No tasks match the current filter"}),e.jsx("span",{children:"Try adjusting your search or filter criteria"})]}):e.jsxs(e.Fragment,{children:[m.map(r=>{const y=d.has(r.id);return e.jsxs("div",{children:[e.jsx(H,{label:r.label,count:r.tasks.length,collapsed:y,onToggle:()=>l(r.id)}),!y&&r.tasks.map(g=>e.jsx(L,{task:g},g.id))]},r.id)}),n.showCompleted&&_.length>0&&e.jsxs("div",{children:[e.jsx(H,{label:"Completed",count:_.length,collapsed:d.has("__completed__"),onToggle:()=>l("__completed__")}),!d.has("__completed__")&&_.map(r=>e.jsx(L,{task:r},r.id))]})]})}),e.jsx(Me,{})]})}export{$e as default};