claude-pager 0.1.7 → 0.2.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.
Files changed (48) hide show
  1. package/README.md +26 -0
  2. package/dist/cli/run.d.ts.map +1 -1
  3. package/dist/cli/run.js +8 -0
  4. package/dist/cli/run.js.map +1 -1
  5. package/dist/cli/setup.d.ts.map +1 -1
  6. package/dist/cli/setup.js +70 -0
  7. package/dist/cli/setup.js.map +1 -1
  8. package/dist/daemon/handlers.d.ts.map +1 -1
  9. package/dist/daemon/handlers.js +10 -2
  10. package/dist/daemon/handlers.js.map +1 -1
  11. package/dist/daemon/server.d.ts.map +1 -1
  12. package/dist/daemon/server.js +94 -6
  13. package/dist/daemon/server.js.map +1 -1
  14. package/dist/dashboard/ci-provider.d.ts +20 -0
  15. package/dist/dashboard/ci-provider.d.ts.map +1 -0
  16. package/dist/dashboard/ci-provider.js +194 -0
  17. package/dist/dashboard/ci-provider.js.map +1 -0
  18. package/dist/dashboard/enricher.d.ts +34 -0
  19. package/dist/dashboard/enricher.d.ts.map +1 -0
  20. package/dist/dashboard/enricher.js +143 -0
  21. package/dist/dashboard/enricher.js.map +1 -0
  22. package/dist/dashboard/git-status.d.ts +7 -0
  23. package/dist/dashboard/git-status.d.ts.map +1 -0
  24. package/dist/dashboard/git-status.js +33 -0
  25. package/dist/dashboard/git-status.js.map +1 -0
  26. package/dist/dashboard/html.d.ts +2 -0
  27. package/dist/dashboard/html.d.ts.map +1 -0
  28. package/dist/dashboard/html.js +878 -0
  29. package/dist/dashboard/html.js.map +1 -0
  30. package/dist/dashboard/routes.d.ts +3 -0
  31. package/dist/dashboard/routes.d.ts.map +1 -0
  32. package/dist/dashboard/routes.js +14 -0
  33. package/dist/dashboard/routes.js.map +1 -0
  34. package/dist/dashboard/transcript.d.ts +10 -0
  35. package/dist/dashboard/transcript.d.ts.map +1 -0
  36. package/dist/dashboard/transcript.js +217 -0
  37. package/dist/dashboard/transcript.js.map +1 -0
  38. package/dist/sessions/events.d.ts +1 -0
  39. package/dist/sessions/events.d.ts.map +1 -1
  40. package/dist/sessions/events.js +13 -0
  41. package/dist/sessions/events.js.map +1 -1
  42. package/dist/sessions/tracker.d.ts +1 -0
  43. package/dist/sessions/tracker.d.ts.map +1 -1
  44. package/dist/sessions/tracker.js +15 -0
  45. package/dist/sessions/tracker.js.map +1 -1
  46. package/dist/types.d.ts +11 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCIProvider = createCIProvider;
4
+ exports.getCIPipelines = getCIPipelines;
5
+ const node_child_process_1 = require("node:child_process");
6
+ // Cache: repoUrl → { data, timestamp }
7
+ const cache = new Map();
8
+ const CACHE_TTL_MS = 30_000;
9
+ function getRemoteUrl(cwd) {
10
+ try {
11
+ const url = (0, node_child_process_1.execFileSync)('git', ['-C', cwd, 'remote', 'get-url', 'origin'], { timeout: 3000 }).toString().trim();
12
+ return url || null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ // --- GitLab Provider ---
19
+ function extractGitLabProjectPath(remoteUrl, serverUrl) {
20
+ // Normalize: strip protocol, credentials, and .git suffix
21
+ // https://oauth2:token@git.example.com/group/project.git → git.example.com/group/project
22
+ // https://git.example.com/group/project.git → git.example.com/group/project
23
+ // git@git.example.com:group/project.git → git.example.com/group/project
24
+ const host = new URL(serverUrl).host;
25
+ const escaped = host.replace(/\./g, '\\.');
26
+ // Match host in URL, capture everything after it
27
+ const match = remoteUrl.match(new RegExp(`${escaped}[/:](.+?)(?:\\.git)?$`));
28
+ if (match)
29
+ return match[1];
30
+ return null;
31
+ }
32
+ // Cache project path → numeric ID to avoid repeated lookups
33
+ const projectIdCache = new Map();
34
+ async function resolveGitLabProjectId(config, projectPath) {
35
+ const cached = projectIdCache.get(projectPath);
36
+ if (cached)
37
+ return cached;
38
+ try {
39
+ const res = await fetch(`${config.url}/api/v4/projects?search=${encodeURIComponent(projectPath.split('/').pop() || '')}`, {
40
+ headers: { 'PRIVATE-TOKEN': config.token },
41
+ signal: AbortSignal.timeout(5000),
42
+ });
43
+ if (!res.ok)
44
+ return null;
45
+ const projects = await res.json();
46
+ const match = projects.find(p => p.path_with_namespace === projectPath);
47
+ if (match) {
48
+ projectIdCache.set(projectPath, match.id);
49
+ return match.id;
50
+ }
51
+ }
52
+ catch {
53
+ // skip
54
+ }
55
+ return null;
56
+ }
57
+ async function gitlabGetPipelines(config, repoUrl, branches) {
58
+ const projectPath = extractGitLabProjectPath(repoUrl, config.url);
59
+ if (!projectPath)
60
+ return {};
61
+ const projectId = await resolveGitLabProjectId(config, projectPath);
62
+ if (!projectId)
63
+ return {};
64
+ const result = {};
65
+ for (const branch of branches) {
66
+ try {
67
+ const apiUrl = `${config.url}/api/v4/projects/${projectId}/pipelines?ref=${encodeURIComponent(branch)}&per_page=1`;
68
+ const res = await fetch(apiUrl, {
69
+ headers: { 'PRIVATE-TOKEN': config.token },
70
+ signal: AbortSignal.timeout(5000),
71
+ });
72
+ if (!res.ok)
73
+ continue;
74
+ const pipelines = await res.json();
75
+ if (pipelines.length > 0) {
76
+ const p = pipelines[0];
77
+ const status = mapGitLabStatus(p.status);
78
+ const key = branch === 'main' || branch === 'master' ? 'main' : 'staging';
79
+ result[key] = {
80
+ status,
81
+ url: p.web_url,
82
+ duration: p.duration || undefined,
83
+ updatedAt: new Date(p.updated_at).getTime(),
84
+ ref: p.ref,
85
+ };
86
+ }
87
+ }
88
+ catch {
89
+ // skip this branch
90
+ }
91
+ }
92
+ return result;
93
+ }
94
+ function mapGitLabStatus(status) {
95
+ switch (status) {
96
+ case 'success': return 'success';
97
+ case 'failed': return 'failed';
98
+ case 'running':
99
+ case 'pending': return 'running';
100
+ case 'canceled': return 'canceled';
101
+ case 'created':
102
+ case 'waiting_for_resource':
103
+ case 'preparing': return 'pending';
104
+ default: return 'unknown';
105
+ }
106
+ }
107
+ // --- GitHub Provider ---
108
+ async function githubGetPipelines(config, repoUrl, branches) {
109
+ // Extract owner/repo from URL
110
+ const match = repoUrl.match(/github\.com[/:](.+?)(?:\.git)?$/);
111
+ if (!match)
112
+ return {};
113
+ const repo = match[1];
114
+ const result = {};
115
+ for (const branch of branches) {
116
+ try {
117
+ const apiUrl = `https://api.github.com/repos/${repo}/actions/runs?branch=${encodeURIComponent(branch)}&per_page=1`;
118
+ const res = await fetch(apiUrl, {
119
+ headers: {
120
+ 'Authorization': `Bearer ${config.token}`,
121
+ 'Accept': 'application/vnd.github+json',
122
+ },
123
+ signal: AbortSignal.timeout(5000),
124
+ });
125
+ if (!res.ok)
126
+ continue;
127
+ const data = await res.json();
128
+ if (data.workflow_runs.length > 0) {
129
+ const run = data.workflow_runs[0];
130
+ const status = mapGitHubStatus(run.status, run.conclusion);
131
+ const key = branch === 'main' || branch === 'master' ? 'main' : 'staging';
132
+ result[key] = {
133
+ status,
134
+ url: run.html_url,
135
+ updatedAt: new Date(run.updated_at).getTime(),
136
+ ref: branch,
137
+ };
138
+ }
139
+ }
140
+ catch {
141
+ // skip
142
+ }
143
+ }
144
+ return result;
145
+ }
146
+ function mapGitHubStatus(status, conclusion) {
147
+ if (status === 'completed') {
148
+ switch (conclusion) {
149
+ case 'success': return 'success';
150
+ case 'failure': return 'failed';
151
+ case 'cancelled': return 'canceled';
152
+ default: return 'unknown';
153
+ }
154
+ }
155
+ if (status === 'in_progress' || status === 'queued')
156
+ return 'running';
157
+ return 'pending';
158
+ }
159
+ // --- Public API ---
160
+ function createCIProvider(config) {
161
+ if (config.type === 'gitlab' && config.gitlab) {
162
+ return {
163
+ name: 'gitlab',
164
+ getPipelines: (repoUrl, branches) => gitlabGetPipelines(config.gitlab, repoUrl, branches),
165
+ };
166
+ }
167
+ if (config.type === 'github' && config.github) {
168
+ return {
169
+ name: 'github',
170
+ getPipelines: (repoUrl, branches) => githubGetPipelines(config.github, repoUrl, branches),
171
+ };
172
+ }
173
+ return null;
174
+ }
175
+ async function getCIPipelines(cwd, ciConfig) {
176
+ if (!ciConfig)
177
+ return {};
178
+ const repoUrl = getRemoteUrl(cwd);
179
+ if (!repoUrl)
180
+ return {};
181
+ // Check cache
182
+ const cached = cache.get(repoUrl);
183
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
184
+ return cached.data;
185
+ }
186
+ const provider = createCIProvider(ciConfig);
187
+ if (!provider)
188
+ return {};
189
+ const branches = ['main', 'master', 'staging'];
190
+ const data = await provider.getPipelines(repoUrl, branches);
191
+ cache.set(repoUrl, { data, timestamp: Date.now() });
192
+ return data;
193
+ }
194
+ //# sourceMappingURL=ci-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-provider.js","sourceRoot":"","sources":["../../src/dashboard/ci-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { GitInfo } from './git-status.js';
2
+ import type { BranchPipelines } from './ci-provider.js';
3
+ export interface DashboardSession {
4
+ sessionId: string;
5
+ title: string;
6
+ state: 'working' | 'waiting_input' | 'waiting_permission' | 'idle' | 'unknown';
7
+ pendingQuestion?: {
8
+ eventId: string;
9
+ shortId: string;
10
+ type: string;
11
+ message: string;
12
+ toolName?: string;
13
+ toolInput?: string;
14
+ agoSeconds: number;
15
+ };
16
+ git: GitInfo;
17
+ needsTesting: boolean;
18
+ committed: boolean;
19
+ pushed: boolean;
20
+ tmuxPane: string;
21
+ lastActivity: number;
22
+ }
23
+ export interface DashboardProject {
24
+ name: string;
25
+ path: string;
26
+ sessions: DashboardSession[];
27
+ ci?: BranchPipelines;
28
+ }
29
+ export interface DashboardResponse {
30
+ projects: DashboardProject[];
31
+ updatedAt: number;
32
+ }
33
+ export declare function getDashboardData(): Promise<DashboardResponse>;
34
+ //# sourceMappingURL=enricher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/dashboard/enricher.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkBxD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,eAAe,GAAG,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAC;IAC/E,eAAe,CAAC,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,GAAG,EAAE,OAAO,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAmInE"}
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDashboardData = getDashboardData;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const tracker_js_1 = require("../sessions/tracker.js");
6
+ const events_js_1 = require("../sessions/events.js");
7
+ const index_js_1 = require("../config/index.js");
8
+ const transcript_js_1 = require("./transcript.js");
9
+ const git_status_js_1 = require("./git-status.js");
10
+ const ci_provider_js_1 = require("./ci-provider.js");
11
+ // Track last title set per pane to avoid unnecessary tmux calls
12
+ const lastPaneTitle = new Map();
13
+ function updatePaneTitle(tmuxPane, title) {
14
+ // Truncate to 30 chars for tab readability
15
+ const short = title.length > 30 ? title.slice(0, 28) + '..' : title;
16
+ if (lastPaneTitle.get(tmuxPane) === short)
17
+ return;
18
+ try {
19
+ (0, node_child_process_1.execFileSync)('tmux', ['rename-window', '-t', tmuxPane, short], { timeout: 1000 });
20
+ lastPaneTitle.set(tmuxPane, short);
21
+ }
22
+ catch {
23
+ // tmux not available or pane not found
24
+ }
25
+ }
26
+ async function getDashboardData() {
27
+ (0, tracker_js_1.cleanDeadSessions)();
28
+ const sessions = (0, tracker_js_1.listSessions)();
29
+ const pending = (0, events_js_1.listPending)();
30
+ const config = (0, index_js_1.loadConfig)();
31
+ const DAY_MS = 24 * 3600_000;
32
+ const enriched = sessions
33
+ .filter(s => s.tmuxPane)
34
+ .map(session => {
35
+ const transcript = (0, transcript_js_1.readTranscriptInfo)(session.sessionId, session.cwd);
36
+ const git = (0, git_status_js_1.getGitStatus)(session.cwd);
37
+ // Find pending question for this session — prioritize permission_prompt over idle_prompt
38
+ const sessionPending = pending.filter(p => p.event.sessionId === session.sessionId);
39
+ let pendingQ = sessionPending.find(p => p.event.type === 'permission_prompt')
40
+ || sessionPending[0];
41
+ // Auto-clear stale pending questions that were answered directly in the terminal
42
+ if (pendingQ) {
43
+ const isStale =
44
+ // Permission prompt: if transcript progressed after notification, it was answered
45
+ (pendingQ.event.type === 'permission_prompt' && transcript.lastTimestamp > pendingQ.notifiedAt + 2000) ||
46
+ // Idle prompt: if Claude is now working again, user already replied
47
+ (pendingQ.event.type === 'idle_prompt' && transcript.state === 'working' && transcript.lastTimestamp > pendingQ.notifiedAt);
48
+ if (isStale) {
49
+ (0, events_js_1.removePending)(pendingQ.event.id);
50
+ pendingQ = undefined;
51
+ }
52
+ }
53
+ // State: pending question overrides transcript state
54
+ let state = transcript.state;
55
+ if (pendingQ) {
56
+ state = pendingQ.event.type === 'permission_prompt'
57
+ ? 'waiting_permission'
58
+ : 'waiting_input';
59
+ }
60
+ // Update tmux pane title with the session title
61
+ if (session.tmuxPane && transcript.title && transcript.title !== 'No transcript') {
62
+ const projectName = session.cwd.split('/').pop() || '';
63
+ updatePaneTitle(session.tmuxPane, `${projectName}: ${transcript.title}`);
64
+ }
65
+ return {
66
+ sessionId: session.sessionId,
67
+ title: transcript.title,
68
+ state,
69
+ pendingQuestion: pendingQ ? {
70
+ eventId: pendingQ.event.id,
71
+ shortId: pendingQ.shortId,
72
+ type: pendingQ.event.type,
73
+ message: pendingQ.event.message.slice(0, 200),
74
+ toolName: pendingQ.event.toolName,
75
+ toolInput: pendingQ.event.toolInput,
76
+ agoSeconds: Math.floor((Date.now() - pendingQ.notifiedAt) / 1000),
77
+ } : undefined,
78
+ git,
79
+ needsTesting: false, // computed at project level after CI fetch
80
+ committed: git.modifiedFiles === 0 || transcript.recentCommit,
81
+ pushed: git.unpushedCommits === 0 || transcript.recentPush,
82
+ tmuxPane: session.tmuxPane || '',
83
+ lastActivity: transcript.lastTimestamp || session.timestamp,
84
+ cwd: session.cwd,
85
+ };
86
+ })
87
+ // Filter out sessions with no transcript that are older than 24h (stale recovered sessions)
88
+ .filter(s => !(s.title === 'No transcript' && Date.now() - s.lastActivity > DAY_MS));
89
+ // Group by project (cwd)
90
+ const projectMap = new Map();
91
+ for (const s of enriched) {
92
+ const existing = projectMap.get(s.cwd) || [];
93
+ existing.push(s);
94
+ projectMap.set(s.cwd, existing);
95
+ }
96
+ const stateOrder = {
97
+ waiting_permission: 0,
98
+ waiting_input: 1,
99
+ working: 2,
100
+ idle: 3,
101
+ unknown: 4,
102
+ };
103
+ // Fetch CI pipelines for each unique cwd (in parallel)
104
+ const uniqueCwds = Array.from(projectMap.keys());
105
+ const ciResults = new Map();
106
+ if (config.ci) {
107
+ const ciPromises = uniqueCwds.map(async (cwd) => {
108
+ const pipelines = await (0, ci_provider_js_1.getCIPipelines)(cwd, config.ci);
109
+ ciResults.set(cwd, pipelines);
110
+ });
111
+ await Promise.all(ciPromises);
112
+ }
113
+ const projects = Array.from(projectMap.entries())
114
+ .map(([path, sessions]) => {
115
+ const ci = ciResults.get(path);
116
+ const git = sessions[0]?.git;
117
+ // needsTesting logic:
118
+ // - CI failed on any branch → needs testing
119
+ // - Has unpushed commits (CI hasn't seen this code yet) → needs testing
120
+ // - No CI configured but has uncommitted changes → needs testing (fallback)
121
+ const ciFailed = ci?.main?.status === 'failed' || ci?.staging?.status === 'failed';
122
+ const ciRunning = ci?.main?.status === 'running' || ci?.staging?.status === 'running';
123
+ const hasUnpushed = git ? git.unpushedCommits > 0 : false;
124
+ const hasCI = !!(ci?.main || ci?.staging);
125
+ const needsTesting = ciFailed || hasUnpushed || (!hasCI && git ? git.modifiedFiles > 0 : false);
126
+ return {
127
+ name: path.split('/').pop() || path,
128
+ path,
129
+ sessions: sessions
130
+ .map(({ cwd: _cwd, ...rest }) => ({ ...rest, needsTesting }))
131
+ .sort((a, b) => (stateOrder[a.state] ?? 5) - (stateOrder[b.state] ?? 5)),
132
+ ci,
133
+ ciRunning,
134
+ };
135
+ })
136
+ .sort((a, b) => {
137
+ const aMin = Math.min(...a.sessions.map(s => stateOrder[s.state] ?? 5));
138
+ const bMin = Math.min(...b.sessions.map(s => stateOrder[s.state] ?? 5));
139
+ return aMin !== bMin ? aMin - bMin : a.name.localeCompare(b.name);
140
+ });
141
+ return { projects, updatedAt: Date.now() };
142
+ }
143
+ //# sourceMappingURL=enricher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enricher.js","sourceRoot":"","sources":["../../src/dashboard/enricher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export interface GitInfo {
2
+ branch: string;
3
+ modifiedFiles: number;
4
+ unpushedCommits: number;
5
+ }
6
+ export declare function getGitStatus(cwd: string): GitInfo;
7
+ //# sourceMappingURL=git-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-status.d.ts","sourceRoot":"","sources":["../../src/dashboard/git-status.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAaD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAqBjD"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getGitStatus = getGitStatus;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const cache = new Map();
6
+ const CACHE_TTL_MS = 10_000;
7
+ function execGit(cwd, args) {
8
+ try {
9
+ return (0, node_child_process_1.execFileSync)('git', ['-C', cwd, ...args], { timeout: 3000 }).toString().trim();
10
+ }
11
+ catch {
12
+ return '';
13
+ }
14
+ }
15
+ function getGitStatus(cwd) {
16
+ const now = Date.now();
17
+ const cached = cache.get(cwd);
18
+ if (cached && now - cached.timestamp < CACHE_TTL_MS) {
19
+ return cached.data;
20
+ }
21
+ const branch = execGit(cwd, ['branch', '--show-current']) || 'unknown';
22
+ const statusOut = execGit(cwd, ['status', '--porcelain']);
23
+ const modifiedFiles = statusOut ? statusOut.split('\n').filter(Boolean).length : 0;
24
+ let unpushedCommits = 0;
25
+ if (branch !== 'unknown') {
26
+ const logOut = execGit(cwd, ['log', '--oneline', `origin/${branch}..HEAD`]);
27
+ unpushedCommits = logOut ? logOut.split('\n').filter(Boolean).length : 0;
28
+ }
29
+ const data = { branch, modifiedFiles, unpushedCommits };
30
+ cache.set(cwd, { data, timestamp: now });
31
+ return data;
32
+ }
33
+ //# sourceMappingURL=git-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-status.js","sourceRoot":"","sources":["../../src/dashboard/git-status.ts"],"names":[],"mappings":";;AAmBA,oCAqBC;AAxCD,2DAAkD;AAQlD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgD,CAAC;AACtE,MAAM,YAAY,GAAG,MAAM,CAAC;AAE5B,SAAS,OAAO,CAAC,GAAW,EAAE,IAAc;IAC1C,IAAI,CAAC;QACH,OAAO,IAAA,iCAAY,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,GAAW;IACtC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,YAAY,EAAE,CAAC;QACpD,OAAO,MAAM,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,IAAI,SAAS,CAAC;IAEvE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC;IAC1D,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnF,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,UAAU,MAAM,QAAQ,CAAC,CAAC,CAAC;QAC5E,eAAe,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,IAAI,GAAY,EAAE,MAAM,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC;IACjE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const DASHBOARD_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>claude-pager dashboard</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: #0d1117;\n color: #c9d1d9;\n font-family: 'JetBrains Mono', monospace;\n min-height: 100vh;\n padding: 24px;\n }\n\n header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 32px;\n padding-bottom: 16px;\n border-bottom: 1px solid #21262d;\n }\n\n .logo {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .logo h1 {\n font-size: 22px;\n font-weight: 700;\n color: #f0f6fc;\n }\n\n .cursor {\n display: inline-block;\n width: 10px;\n height: 20px;\n background: #58a6ff;\n animation: blink 1s step-end infinite;\n vertical-align: middle;\n margin-left: 4px;\n }\n\n @keyframes blink {\n 50% { opacity: 0; }\n }\n\n .status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .status-dot.connected { background: #3fb950; box-shadow: 0 0 6px #3fb950; }\n .status-dot.disconnected { background: #f85149; box-shadow: 0 0 6px #f85149; }\n\n .meta {\n font-size: 12px;\n color: #484f58;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .project {\n margin-bottom: 28px;\n }\n\n .project-header {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 12px;\n }\n\n .project-header h2 {\n font-size: 16px;\n font-weight: 600;\n color: #58a6ff;\n }\n\n .project-count {\n background: #21262d;\n color: #8b949e;\n font-size: 11px;\n padding: 2px 8px;\n border-radius: 10px;\n }\n\n .project-path {\n font-size: 11px;\n color: #484f58;\n margin-left: auto;\n }\n\n .pin-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 14px;\n opacity: 0.3;\n transition: opacity 0.2s;\n padding: 2px 4px;\n }\n\n .pin-btn:hover { opacity: 0.7; }\n .pin-btn.pinned { opacity: 1; }\n\n .dismiss-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 12px;\n opacity: 0.25;\n transition: opacity 0.2s;\n padding: 2px 4px;\n }\n\n .dismiss-btn:hover { opacity: 0.8; color: #f85149; }\n\n .ci-row {\n display: flex;\n gap: 12px;\n margin-bottom: 12px;\n font-size: 11px;\n }\n\n .ci-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 3px 10px;\n border-radius: 12px;\n font-weight: 600;\n text-decoration: none;\n transition: opacity 0.2s;\n }\n\n .ci-badge:hover { opacity: 0.8; }\n\n .ci-badge.success { background: #0d2818; color: #3fb950; }\n .ci-badge.failed { background: #490202; color: #f85149; }\n .ci-badge.running { background: #0d419d; color: #58a6ff; animation: pulse 2s ease-in-out infinite; }\n .ci-badge.pending { background: #3d2e00; color: #d29922; }\n .ci-badge.canceled { background: #21262d; color: #8b949e; }\n .ci-badge.unknown { background: #21262d; color: #484f58; }\n\n .ci-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .ci-dot.success { background: #3fb950; }\n .ci-dot.failed { background: #f85149; }\n .ci-dot.running { background: #58a6ff; }\n .ci-dot.pending { background: #d29922; }\n .ci-dot.canceled { background: #8b949e; }\n .ci-dot.unknown { background: #484f58; }\n\n .sessions {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));\n gap: 12px;\n }\n\n .card {\n background: #161b22;\n border: 1px solid #21262d;\n border-radius: 8px;\n padding: 16px;\n transition: border-color 0.2s, box-shadow 0.2s, opacity 0.3s;\n }\n\n .card:hover {\n border-color: #388bfd44;\n box-shadow: 0 0 12px #388bfd22;\n }\n\n .card.stale {\n opacity: 0.45;\n border-style: dashed;\n }\n\n .card.stale:hover {\n opacity: 0.8;\n }\n\n .card.active {\n border-color: #388bfd44;\n border-left: 3px solid #58a6ff;\n }\n\n .card.alert {\n border-color: #f0883e44;\n border-left: 3px solid #f0883e;\n }\n\n .card-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n margin-bottom: 10px;\n }\n\n .card-title {\n font-size: 14px;\n font-weight: 600;\n color: #f0f6fc;\n line-height: 1.3;\n flex: 1;\n margin-right: 8px;\n overflow: hidden;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n }\n\n .card-title.expanded {\n -webkit-line-clamp: unset;\n overflow: visible;\n white-space: pre-wrap;\n }\n\n .expand-btn {\n background: none;\n border: none;\n color: #58a6ff;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n cursor: pointer;\n padding: 2px 0;\n opacity: 0.8;\n }\n\n .expand-btn:hover { opacity: 1; }\n\n .badge {\n font-size: 10px;\n font-weight: 600;\n padding: 3px 8px;\n border-radius: 12px;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .badge.working {\n background: #0d419d;\n color: #58a6ff;\n animation: pulse 2s ease-in-out infinite;\n }\n\n .badge.waiting_permission {\n background: #5a1e02;\n color: #f0883e;\n }\n\n .badge.waiting_input {\n background: #3d2e00;\n color: #d29922;\n }\n\n .badge.idle {\n background: #21262d;\n color: #8b949e;\n }\n\n .badge.unknown {\n background: #21262d;\n color: #484f58;\n }\n\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n }\n\n .pending-box {\n background: #1c1208;\n border: 1px solid #3d2e00;\n border-radius: 6px;\n padding: 8px 10px;\n margin-bottom: 10px;\n font-size: 12px;\n color: #d29922;\n }\n\n .pending-box .tool {\n color: #f0883e;\n font-weight: 600;\n }\n\n .pending-box .ago {\n color: #8b949e;\n float: right;\n }\n\n .action-row {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n }\n\n .action-btn {\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 6px;\n border: none;\n cursor: pointer;\n transition: opacity 0.2s, transform 0.1s;\n }\n\n .action-btn:hover { opacity: 0.85; }\n .action-btn:active { transform: scale(0.96); }\n\n .action-btn.allow {\n background: #238636;\n color: #ffffff;\n }\n\n .action-btn.deny {\n background: #da3633;\n color: #ffffff;\n }\n\n .action-btn.allow-all {\n background: #1f6feb;\n color: #ffffff;\n margin-left: auto;\n }\n\n .action-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n\n .reply-input {\n flex: 1;\n font-family: 'JetBrains Mono', monospace;\n font-size: 11px;\n padding: 4px 10px;\n border-radius: 6px;\n border: 1px solid #30363d;\n background: #0d1117;\n color: #c9d1d9;\n outline: none;\n }\n\n .reply-input:focus {\n border-color: #58a6ff;\n }\n\n .git-row {\n display: flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n margin-bottom: 6px;\n }\n\n .git-branch {\n color: #8b949e;\n }\n\n .git-branch::before {\n content: '\u2387 ';\n }\n\n .git-modified {\n color: #f85149;\n }\n\n .git-unpushed {\n color: #d29922;\n }\n\n .git-clean {\n color: #3fb950;\n }\n\n .needs-testing {\n display: inline-block;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n background: #490202;\n color: #f85149;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .flag {\n display: inline-block;\n font-size: 10px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n letter-spacing: 0.3px;\n }\n\n .flag.ok {\n background: #0d2818;\n color: #3fb950;\n }\n\n .flag.pending {\n background: #3d2e00;\n color: #d29922;\n }\n\n .card-footer {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 8px;\n font-size: 10px;\n color: #484f58;\n }\n\n .card-footer .spacer {\n margin-left: auto;\n }\n\n .empty {\n text-align: center;\n padding: 60px 20px;\n color: #484f58;\n }\n\n .empty h2 {\n font-size: 18px;\n color: #8b949e;\n margin-bottom: 8px;\n }\n\n .empty p {\n font-size: 13px;\n }\n\n @media (max-width: 768px) {\n body { padding: 12px; }\n\n header { flex-direction: column; align-items: flex-start; gap: 8px; }\n\n .logo h1 { font-size: 18px; }\n\n .sessions {\n grid-template-columns: 1fr;\n gap: 10px;\n }\n\n .project-header {\n flex-wrap: wrap;\n }\n\n .project-path { display: none; }\n\n .card { padding: 12px; }\n\n .card-title { font-size: 13px; }\n\n .action-btn {\n padding: 8px 18px;\n font-size: 13px;\n }\n\n .reply-input {\n font-size: 13px;\n padding: 8px 10px;\n }\n\n .ci-row { flex-wrap: wrap; gap: 6px; }\n\n .pending-box { font-size: 11px; }\n\n .pending-box code { font-size: 9px; }\n }\n\n @media (max-width: 480px) {\n body { padding: 8px; }\n\n .logo h1 { font-size: 16px; }\n\n .badge { font-size: 9px; padding: 2px 6px; }\n\n .git-row { flex-wrap: wrap; gap: 6px; }\n\n .action-btn {\n padding: 10px 20px;\n font-size: 14px;\n }\n\n .action-btn.allow-all {\n width: 100%;\n text-align: center;\n }\n }\n\n .scanline {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.03) 2px,\n rgba(0, 0, 0, 0.03) 4px\n );\n z-index: 999;\n }\n </style>\n</head>\n<body>\n <div class=\"scanline\"></div>\n <header>\n <div class=\"logo\">\n <h1>claude-pager<span class=\"cursor\"></span></h1>\n </div>\n <div class=\"meta\">\n <button class=\"action-btn allow-all\" id=\"allowAllBtn\" style=\"display:none\" onclick=\"allowAll()\">Allow All</button>\n <span class=\"status-dot connected\" id=\"statusDot\"></span>\n <span id=\"lastUpdate\">connecting...</span>\n </div>\n </header>\n <main id=\"projects\"></main>\n\n <script>\n let data = null;\n\n function getPinnedOrder() {\n try { return JSON.parse(localStorage.getItem('dashboard-pin-order') || '[]'); }\n catch { return []; }\n }\n\n function savePinnedOrder(order) {\n localStorage.setItem('dashboard-pin-order', JSON.stringify(order));\n }\n\n function togglePin(name) {\n const order = getPinnedOrder();\n const idx = order.indexOf(name);\n if (idx >= 0) {\n order.splice(idx, 1);\n } else {\n order.push(name);\n }\n savePinnedOrder(order);\n if (data) render(data);\n }\n\n function sortProjects(projects) {\n const pinned = getPinnedOrder();\n return [...projects].sort((a, b) => {\n const aPin = pinned.indexOf(a.name);\n const bPin = pinned.indexOf(b.name);\n const aIsPinned = aPin >= 0;\n const bIsPinned = bPin >= 0;\n // Pinned projects first, in their pinned order\n if (aIsPinned && bIsPinned) return aPin - bPin;\n if (aIsPinned) return -1;\n if (bIsPinned) return 1;\n // Unpinned: keep the original sort (by state)\n return 0;\n });\n }\n\n function escapeHtml(s) {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n }\n\n function timeAgo(epochMs) {\n if (!epochMs) return 'unknown';\n const s = Math.floor((Date.now() - epochMs) / 1000);\n if (s < 10) return 'just now';\n if (s < 60) return s + 's ago';\n if (s < 3600) return Math.floor(s / 60) + 'm ago';\n if (s < 86400) return Math.floor(s / 3600) + 'h ago';\n return Math.floor(s / 86400) + 'd ago';\n }\n\n function stateLabel(state) {\n const labels = {\n working: 'Working',\n waiting_permission: 'Permission',\n waiting_input: 'Waiting',\n idle: 'Idle',\n unknown: '?',\n };\n return labels[state] || state;\n }\n\n function renderSession(s) {\n const ageMs = Date.now() - s.lastActivity;\n const isStale = (s.state === 'idle' || s.state === 'unknown') && ageMs > 2 * 3600_000;\n const isAlert = s.state === 'waiting_permission' || s.state === 'waiting_input';\n const isActive = s.state === 'working';\n const cardClass = isStale ? 'stale' : isAlert ? 'alert' : isActive ? 'active' : '';\n\n let pending = '';\n if (s.pendingQuestion) {\n const q = s.pendingQuestion;\n const isPermission = q.type === 'permission_prompt';\n const toolInfo = q.toolName\n ? '<span class=\"tool\">' + escapeHtml(q.toolName) + '</span>' +\n (q.toolInput ? '<br><code style=\"font-size:10px;color:#8b949e;word-break:break-all\">' + escapeHtml(q.toolInput.slice(0, 200)) + '</code>' : '')\n : escapeHtml(q.message.slice(0, 150));\n\n const actions = isPermission\n ? `<div class=\"action-row\">\n <button class=\"action-btn allow\" onclick=\"respondTo('${q.eventId}', 'allow', this)\">\u2713 Allow</button>\n <button class=\"action-btn deny\" onclick=\"respondTo('${q.eventId}', 'deny', this)\">\u2717 Deny</button>\n </div>`\n : `<div class=\"action-row\" style=\"align-items:center\">\n <input type=\"text\" class=\"reply-input\" id=\"reply-${q.eventId}\" placeholder=\"Type a reply...\" onkeydown=\"if(event.key==='Enter')respondTo('${q.eventId}',this.value,this)\">\n <button class=\"action-btn allow\" onclick=\"respondTo('${q.eventId}',document.getElementById('reply-${q.eventId}').value,this)\">Send</button>\n </div>`;\n\n pending = `\n <div class=\"pending-box\">\n <span class=\"ago\">${timeAgo(Date.now() - q.agoSeconds * 1000)}</span>\n ${toolInfo}\n ${actions}\n </div>\n `;\n }\n\n const hasGit = s.git.branch !== 'unknown';\n\n const gitParts = [];\n if (hasGit) {\n gitParts.push('<span class=\"git-branch\">' + escapeHtml(s.git.branch) + '</span>');\n gitParts.push(s.git.modifiedFiles > 0\n ? '<span class=\"git-modified\">' + s.git.modifiedFiles + ' mod</span>'\n : '<span class=\"git-clean\">clean</span>');\n if (s.git.unpushedCommits > 0) gitParts.push('<span class=\"git-unpushed\">' + s.git.unpushedCommits + ' unpush</span>');\n gitParts.push(s.committed\n ? '<span class=\"flag ok\">\u2713 commit</span>'\n : '<span class=\"flag pending\">\u25CB uncommit</span>');\n gitParts.push(s.pushed\n ? '<span class=\"flag ok\">\u2713 push</span>'\n : '<span class=\"flag pending\">\u25CB unpush</span>');\n }\n\n // Show reply input for idle/waiting sessions without a pending question\n const idleInput = (!s.pendingQuestion && (s.state === 'idle' || s.state === 'waiting_input' || s.state === 'unknown'))\n ? `<div class=\"action-row\" style=\"margin-top:6px\">\n <input type=\"text\" class=\"reply-input\" id=\"idle-${s.sessionId}\" placeholder=\"Send a message...\" onkeydown=\"if(event.key==='Enter')sendToSession('${s.sessionId}',this.value,this)\">\n <button class=\"action-btn allow\" onclick=\"sendToSession('${s.sessionId}',document.getElementById('idle-${s.sessionId}').value,this)\">Send</button>\n </div>`\n : '';\n\n const titleId = 'title-' + s.sessionId.slice(0, 8);\n const longTitle = s.title.length > 80;\n const expandBtn = longTitle ? `<button class=\"expand-btn\" onclick=\"document.getElementById('${titleId}').classList.toggle('expanded');this.textContent=this.textContent==='...'?'\u25B2':'...'\">...</button>` : '';\n\n return `\n <div class=\"card ${cardClass}\">\n <div class=\"card-header\">\n <span class=\"card-title\" id=\"${titleId}\">${escapeHtml(s.title)}</span>\n <span class=\"badge ${s.state}\">${stateLabel(s.state)}</span>\n <button class=\"dismiss-btn\" onclick=\"dismissSession('${s.sessionId}')\" title=\"Dismiss session\">\uD83D\uDDD1</button>\n </div>\n ${expandBtn}\n ${pending}\n ${idleInput}\n <div class=\"card-footer\">\n ${gitParts.join(' ')}\n <span class=\"spacer\"></span>\n <span>pane ${escapeHtml(s.tmuxPane)}</span>\n <span>${timeAgo(s.lastActivity)}</span>\n </div>\n </div>\n `;\n }\n\n function renderPipeline(label, pipeline) {\n if (!pipeline) return '';\n const s = pipeline.status;\n const dot = '<span class=\"ci-dot ' + s + '\"></span>';\n const text = label + ': ' + s;\n if (pipeline.url) {\n return '<a class=\"ci-badge ' + s + '\" href=\"' + escapeHtml(pipeline.url) + '\" target=\"_blank\">' + dot + ' ' + text + '</a>';\n }\n return '<span class=\"ci-badge ' + s + '\">' + dot + ' ' + text + '</span>';\n }\n\n function renderCI(ci) {\n if (!ci) return '';\n const main = renderPipeline('main', ci.main);\n const staging = renderPipeline('staging', ci.staging);\n return main + staging;\n }\n\n function renderProject(p) {\n const isPinned = getPinnedOrder().includes(p.name);\n const anyNeedsTesting = p.sessions.some(s => s.needsTesting);\n const testBadge = anyNeedsTesting ? '<span class=\"needs-testing\">needs testing</span>' : '';\n const ciBadges = renderCI(p.ci);\n const infoRow = (ciBadges || testBadge) ? '<div class=\"ci-row\">' + ciBadges + testBadge + '</div>' : '';\n\n return `\n <div class=\"project\">\n <div class=\"project-header\">\n <button class=\"pin-btn ${isPinned ? 'pinned' : ''}\" onclick=\"togglePin('${escapeHtml(p.name)}')\" title=\"${isPinned ? 'Unpin' : 'Pin'}\">${isPinned ? '\uD83D\uDCCC' : '\uD83D\uDCCC'}</button>\n <h2>${escapeHtml(p.name)}</h2>\n <span class=\"project-count\">${p.sessions.length} session${p.sessions.length > 1 ? 's' : ''}</span>\n <span class=\"project-path\">${escapeHtml(p.path)}</span>\n </div>\n ${infoRow}\n <div class=\"sessions\">\n ${p.sessions.map(renderSession).join('')}\n </div>\n </div>\n `;\n }\n\n function countPending(data) {\n let count = 0;\n for (const p of data.projects) {\n for (const s of p.sessions) {\n if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') count++;\n }\n }\n return count;\n }\n\n function render(data) {\n const container = document.getElementById('projects');\n if (!data.projects || data.projects.length === 0) {\n container.innerHTML = `\n <div class=\"empty\">\n <h2>No active sessions</h2>\n <p>Start Claude Code in tmux and sessions will appear here.</p>\n </div>\n `;\n document.getElementById('allowAllBtn').style.display = 'none';\n return;\n }\n\n const pendingCount = countPending(data);\n const allowAllBtn = document.getElementById('allowAllBtn');\n if (pendingCount > 1) {\n allowAllBtn.style.display = 'inline-block';\n allowAllBtn.textContent = 'Allow All (' + pendingCount + ')';\n } else {\n allowAllBtn.style.display = 'none';\n }\n\n // Skip DOM update if user is typing in an input field\n if (document.activeElement && document.activeElement.tagName === 'INPUT') return;\n\n // Preserve expanded title state across re-renders\n const expandedTitles = new Set();\n document.querySelectorAll('.card-title.expanded').forEach(el => expandedTitles.add(el.id));\n\n container.innerHTML = sortProjects(data.projects).map(renderProject).join('');\n\n // Restore expanded titles\n expandedTitles.forEach(id => {\n const el = document.getElementById(id);\n if (el) {\n el.classList.add('expanded');\n const btn = el.parentElement?.querySelector('.expand-btn');\n if (btn) btn.textContent = '\u25B2';\n }\n });\n }\n\n async function respondTo(eventId, response, btn) {\n if (btn) btn.disabled = true;\n try {\n const res = await fetch('/api/v1/respond-to', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ eventId, response }),\n });\n if (res.ok) {\n fetchDashboard();\n } else {\n const err = await res.json();\n console.error('respond-to failed:', err);\n }\n } catch (e) {\n console.error('respond-to error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n async function dismissSession(sessionId) {\n try {\n await fetch('/api/v1/dismiss-session', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId }),\n });\n fetchDashboard();\n } catch (e) {\n console.error('dismiss error:', e);\n }\n }\n\n async function sendToSession(sessionId, text, btn) {\n if (!text || !text.trim()) return;\n if (btn) btn.disabled = true;\n try {\n const res = await fetch('/api/v1/send-to', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sessionId, text: text.trim() }),\n });\n if (res.ok) {\n fetchDashboard();\n } else {\n const err = await res.json();\n console.error('send-to failed:', err);\n }\n } catch (e) {\n console.error('send-to error:', e);\n }\n if (btn) btn.disabled = false;\n }\n\n async function allowAll() {\n if (!data) return;\n const pending = [];\n for (const p of data.projects) {\n for (const s of p.sessions) {\n if (s.pendingQuestion && s.pendingQuestion.type === 'permission_prompt') {\n pending.push(s.pendingQuestion);\n }\n }\n }\n for (const q of pending) {\n await respondTo(q.eventId, 'allow', null);\n }\n fetchDashboard();\n }\n\n async function fetchDashboard() {\n try {\n const res = await fetch('/api/v1/dashboard');\n data = await res.json();\n render(data);\n document.getElementById('statusDot').className = 'status-dot connected';\n document.getElementById('lastUpdate').textContent = 'updated ' + timeAgo(data.updatedAt);\n } catch {\n document.getElementById('statusDot').className = 'status-dot disconnected';\n document.getElementById('lastUpdate').textContent = 'disconnected';\n }\n }\n\n fetchDashboard();\n setInterval(fetchDashboard, 2000);\n </script>\n</body>\n</html>";
2
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,46vBAy2BnB,CAAC"}