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.
- package/dist/client/assets/AgendaView-CO7TtOek.js +1 -0
- package/dist/client/assets/AgendaView-CtgBRlOH.css +1 -0
- package/dist/client/assets/{CyberdromeScene-Cz9rmWq-.js → CyberdromeScene-By2Y9cKO.js} +1 -1
- package/dist/client/assets/{HistoryView-CDYT3hYC.js → HistoryView-Dt-Rfqog.js} +1 -1
- package/dist/client/assets/{ProjectBrowserView-DPo6yCZn.js → ProjectBrowserView-BmNIznfT.js} +1 -1
- package/dist/client/assets/{QueueView-De5sOqLj.js → QueueView-Y4HCXfef.js} +1 -1
- package/dist/client/assets/index-C13EUDCm.js +206 -0
- package/dist/client/assets/index-DN-ee9dO.css +1 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/server/apiRouter.ts +99 -2
- package/server/config.ts +16 -0
- package/server/sessionStore.ts +52 -1
- package/server/sshManager.ts +48 -2
- package/dist/client/assets/AgendaView-CuxP5XbK.css +0 -1
- package/dist/client/assets/AgendaView-Doy-5xQR.js +0 -1
- package/dist/client/assets/index-BBpn64S4.css +0 -1
- package/dist/client/assets/index-DMLiHQ-O.js +0 -206
package/server/apiRouter.ts
CHANGED
|
@@ -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
|
+
}
|
package/server/sessionStore.ts
CHANGED
|
@@ -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); }
|
package/server/sshManager.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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};
|