@bakapiano/ccsm 0.5.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -38
- package/bin/ccsm.js +194 -0
- package/lib/config.js +1 -0
- package/lib/favorites.js +23 -45
- package/lib/focus.js +90 -14
- package/lib/jsonStore.js +60 -0
- package/lib/labels.js +29 -0
- package/lib/webTerminal.js +173 -0
- package/lib/workspace.js +8 -4
- package/package.json +11 -3
- package/public/css/base.css +82 -0
- package/public/css/cards.css +149 -0
- package/public/css/feedback.css +219 -0
- package/public/css/forms.css +282 -0
- package/public/css/layout.css +107 -0
- package/public/css/modal.css +169 -0
- package/public/css/responsive.css +10 -0
- package/public/css/sidebar.css +165 -0
- package/public/css/tables.css +266 -0
- package/public/css/terminals.css +112 -0
- package/public/css/tokens.css +63 -0
- package/public/css/wco.css +70 -0
- package/public/css/widgets.css +204 -0
- package/public/favicon.svg +18 -0
- package/public/index.html +53 -379
- package/public/js/actions.js +87 -0
- package/public/js/api.js +103 -0
- package/public/js/backend.js +28 -0
- package/public/js/components/App.js +45 -0
- package/public/js/components/Card.js +24 -0
- package/public/js/components/DialogHost.js +45 -0
- package/public/js/components/Fab.js +11 -0
- package/public/js/components/FavoritesTable.js +81 -0
- package/public/js/components/Footer.js +12 -0
- package/public/js/components/NewSessionModal.js +142 -0
- package/public/js/components/OfflineBanner.js +52 -0
- package/public/js/components/PageHead.js +33 -0
- package/public/js/components/Pagination.js +27 -0
- package/public/js/components/ProgressList.js +32 -0
- package/public/js/components/RecentTable.js +68 -0
- package/public/js/components/RepoPicker.js +40 -0
- package/public/js/components/ReposEditor.js +74 -0
- package/public/js/components/ServerStatus.js +18 -0
- package/public/js/components/SessionsTable.js +71 -0
- package/public/js/components/Sidebar.js +52 -0
- package/public/js/components/SnapshotPanel.js +77 -0
- package/public/js/components/TerminalView.js +108 -0
- package/public/js/components/TitleCell.js +40 -0
- package/public/js/components/Toast.js +8 -0
- package/public/js/components/WorkspacePicker.js +19 -0
- package/public/js/components/WorkspacesGrid.js +41 -0
- package/public/js/dialog.js +59 -0
- package/public/js/html.js +6 -0
- package/public/js/icons.js +114 -0
- package/public/js/main.js +81 -0
- package/public/js/pages/AboutPage.js +85 -0
- package/public/js/pages/ConfigurePage.js +194 -0
- package/public/js/pages/LaunchPage.js +117 -0
- package/public/js/pages/SessionsPage.js +47 -0
- package/public/js/pages/TerminalsPage.js +74 -0
- package/public/js/state.js +87 -0
- package/public/js/streaming.js +96 -0
- package/public/js/toast.js +14 -0
- package/public/js/util.js +24 -0
- package/public/manifest.webmanifest +14 -0
- package/scripts/install.js +111 -0
- package/scripts/uninstall.js +56 -0
- package/server.js +314 -31
- package/public/app.js +0 -894
- package/public/styles.css +0 -1204
package/public/index.html
CHANGED
|
@@ -3,392 +3,66 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
|
|
6
|
+
<!-- Bleeds the cream surface into the Edge/Chrome --app= title bar
|
|
7
|
+
so it visually disappears against the body. The browser does
|
|
8
|
+
honor this in standalone app windows. -->
|
|
9
|
+
<meta name="theme-color" content="#faf9f5" />
|
|
10
|
+
<meta name="color-scheme" content="light" />
|
|
11
|
+
<title>CCSM — Claude CLI Sessions Manager</title>
|
|
12
|
+
<!-- All asset paths are RELATIVE so the same index.html works when
|
|
13
|
+
served from localhost:7777/ (backend bundle) AND from
|
|
14
|
+
https://bakapiano.github.io/cssm/v1/ (GH Pages hosted). -->
|
|
15
|
+
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
16
|
+
<link rel="manifest" href="./manifest.webmanifest" />
|
|
7
17
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
18
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
19
|
<link
|
|
10
20
|
rel="stylesheet"
|
|
11
21
|
href="https://fonts.googleapis.com/css2?family=Geist:wght@300..700&family=JetBrains+Mono:wght@400..600&display=swap"
|
|
12
22
|
/>
|
|
13
|
-
<link rel="stylesheet" href="/
|
|
23
|
+
<link rel="stylesheet" href="./css/tokens.css" />
|
|
24
|
+
<link rel="stylesheet" href="./css/base.css" />
|
|
25
|
+
<link rel="stylesheet" href="./css/layout.css" />
|
|
26
|
+
<link rel="stylesheet" href="./css/sidebar.css" />
|
|
27
|
+
<link rel="stylesheet" href="./css/cards.css" />
|
|
28
|
+
<link rel="stylesheet" href="./css/tables.css" />
|
|
29
|
+
<link rel="stylesheet" href="./css/forms.css" />
|
|
30
|
+
<link rel="stylesheet" href="./css/widgets.css" />
|
|
31
|
+
<link rel="stylesheet" href="./css/feedback.css" />
|
|
32
|
+
<link rel="stylesheet" href="./css/modal.css" />
|
|
33
|
+
<link rel="stylesheet" href="./css/terminals.css" />
|
|
34
|
+
<link rel="stylesheet" href="./css/wco.css" />
|
|
35
|
+
<link rel="stylesheet" href="./css/responsive.css" />
|
|
36
|
+
|
|
37
|
+
<script type="importmap">
|
|
38
|
+
{
|
|
39
|
+
"imports": {
|
|
40
|
+
"preact": "https://esm.sh/preact@10.27.0",
|
|
41
|
+
"preact/hooks": "https://esm.sh/preact@10.27.0/hooks",
|
|
42
|
+
"@preact/signals": "https://esm.sh/@preact/signals@1.3.2?deps=preact@10.27.0",
|
|
43
|
+
"htm": "https://esm.sh/htm@3.1.1",
|
|
44
|
+
"@xterm/xterm": "https://esm.sh/@xterm/xterm@5.5.0",
|
|
45
|
+
"@xterm/addon-fit": "https://esm.sh/@xterm/addon-fit@0.10.0?deps=@xterm/xterm@5.5.0",
|
|
46
|
+
"@xterm/addon-web-links": "https://esm.sh/@xterm/addon-web-links@0.11.0?deps=@xterm/xterm@5.5.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
<link rel="stylesheet" href="https://esm.sh/@xterm/xterm@5.5.0/css/xterm.css" />
|
|
14
51
|
</head>
|
|
15
52
|
<body>
|
|
16
|
-
<div
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<nav class="sidebar-nav" role="tablist" aria-label="Sections">
|
|
31
|
-
<button class="nav-item" data-tab="sessions" role="tab" aria-selected="true">
|
|
32
|
-
<span class="nav-icon" aria-hidden="true">
|
|
33
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
34
|
-
<line x1="3" y1="6" x2="21" y2="6"/>
|
|
35
|
-
<line x1="3" y1="12" x2="21" y2="12"/>
|
|
36
|
-
<line x1="3" y1="18" x2="14" y2="18"/>
|
|
37
|
-
</svg>
|
|
38
|
-
</span>
|
|
39
|
-
<span class="nav-label">Sessions</span>
|
|
40
|
-
<span class="nav-badge" id="navCount-sessions">0</span>
|
|
41
|
-
</button>
|
|
42
|
-
<button class="nav-item" data-tab="launch" role="tab" aria-selected="false">
|
|
43
|
-
<span class="nav-icon" aria-hidden="true">
|
|
44
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
45
|
-
<path d="M7 17L17 7"/>
|
|
46
|
-
<path d="M9 7h8v8"/>
|
|
47
|
-
</svg>
|
|
48
|
-
</span>
|
|
49
|
-
<span class="nav-label">Launch</span>
|
|
50
|
-
</button>
|
|
51
|
-
<button class="nav-item" data-tab="configure" role="tab" aria-selected="false">
|
|
52
|
-
<span class="nav-icon" aria-hidden="true">
|
|
53
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
54
|
-
<circle cx="12" cy="12" r="3"/>
|
|
55
|
-
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
56
|
-
</svg>
|
|
57
|
-
</span>
|
|
58
|
-
<span class="nav-label">Configure</span>
|
|
59
|
-
</button>
|
|
60
|
-
</nav>
|
|
61
|
-
|
|
62
|
-
<div class="sidebar-divider"></div>
|
|
63
|
-
|
|
64
|
-
<div class="sidebar-utility">
|
|
65
|
-
<button class="util-item" id="refreshBtn" title="refresh">
|
|
66
|
-
<span class="nav-icon" aria-hidden="true">
|
|
67
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
68
|
-
<polyline points="23 4 23 10 17 10"/>
|
|
69
|
-
<polyline points="1 20 1 14 7 14"/>
|
|
70
|
-
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
71
|
-
</svg>
|
|
72
|
-
</span>
|
|
73
|
-
<span class="nav-label">Refresh</span>
|
|
74
|
-
</button>
|
|
75
|
-
<button class="util-item util-accent" id="finderBtn" title="open a Claude session pointed at ccsm data">
|
|
76
|
-
<span class="nav-icon" aria-hidden="true">
|
|
77
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
78
|
-
<circle cx="11" cy="11" r="7"/>
|
|
79
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
80
|
-
</svg>
|
|
81
|
-
</span>
|
|
82
|
-
<span class="nav-label">Ask Claude</span>
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div class="sidebar-foot">
|
|
87
|
-
<button class="collapse-toggle" id="collapseBtn" aria-label="collapse sidebar">
|
|
88
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
89
|
-
<polyline points="15 18 9 12 15 6"/>
|
|
90
|
-
</svg>
|
|
91
|
-
</button>
|
|
92
|
-
</div>
|
|
93
|
-
</aside>
|
|
94
|
-
|
|
95
|
-
<!-- ─────────── Main ─────────── -->
|
|
96
|
-
<main class="main">
|
|
97
|
-
<header class="page-head">
|
|
98
|
-
<div class="page-head-inner">
|
|
99
|
-
<h1 class="page-title" id="pageTitle">Sessions</h1>
|
|
100
|
-
<p class="page-subtitle" id="pageSubtitle">Live and recently-closed Claude Code sessions on this machine.</p>
|
|
101
|
-
</div>
|
|
102
|
-
<div class="page-head-meta">
|
|
103
|
-
<span class="ph-stat"><span class="ph-key">Port</span> <span class="ph-val" id="hdPort">—</span></span>
|
|
104
|
-
<span class="ph-divider">·</span>
|
|
105
|
-
<span class="ph-stat"><span class="ph-key">Terminal</span> <span class="ph-val" id="hdTerminal">—</span></span>
|
|
106
|
-
<span class="ph-divider">·</span>
|
|
107
|
-
<span class="ph-stat"><span class="ph-key">Now</span> <span class="ph-val" id="hdTime">—</span></span>
|
|
108
|
-
</div>
|
|
109
|
-
</header>
|
|
110
|
-
|
|
111
|
-
<div class="content">
|
|
112
|
-
<!-- ─── Sessions tab ─── -->
|
|
113
|
-
<section class="tab-panel" data-panel="sessions" data-active>
|
|
114
|
-
<div class="page-actions">
|
|
115
|
-
<span class="page-actions-hint">Looking through your past conversations?</span>
|
|
116
|
-
<button class="action primary" id="finderInlineBtn" title="open a Claude session with context on the ccsm data dir">
|
|
117
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
118
|
-
<circle cx="11" cy="11" r="7"/>
|
|
119
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
120
|
-
</svg>
|
|
121
|
-
Ask Claude to find a session
|
|
122
|
-
</button>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<article class="card" id="favoritesCard">
|
|
126
|
-
<header class="card-head">
|
|
127
|
-
<div class="card-titles">
|
|
128
|
-
<h2 class="card-title">
|
|
129
|
-
<svg class="title-icon" viewBox="0 0 24 24" width="15" height="15" fill="currentColor" stroke="none" aria-hidden="true">
|
|
130
|
-
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
|
131
|
-
</svg>
|
|
132
|
-
Favorites
|
|
133
|
-
</h2>
|
|
134
|
-
<p class="card-meta" id="favoritesMeta">click ☆ on any row to pin sessions here</p>
|
|
135
|
-
</div>
|
|
136
|
-
</header>
|
|
137
|
-
<div class="card-body card-body-flush">
|
|
138
|
-
<div class="table-scroll">
|
|
139
|
-
<table class="data" id="favoritesTable">
|
|
140
|
-
<thead>
|
|
141
|
-
<tr>
|
|
142
|
-
<th>Title</th>
|
|
143
|
-
<th>Working directory</th>
|
|
144
|
-
<th>Branch</th>
|
|
145
|
-
<th class="num">Pinned</th>
|
|
146
|
-
<th class="col-actions"></th>
|
|
147
|
-
</tr>
|
|
148
|
-
</thead>
|
|
149
|
-
<tbody></tbody>
|
|
150
|
-
</table>
|
|
151
|
-
</div>
|
|
152
|
-
<div class="empty" id="favoritesEmpty">No favorites yet. Star a session row to pin it here.</div>
|
|
153
|
-
</div>
|
|
154
|
-
</article>
|
|
155
|
-
|
|
156
|
-
<article class="card">
|
|
157
|
-
<header class="card-head">
|
|
158
|
-
<div class="card-titles">
|
|
159
|
-
<h2 class="card-title">Live sessions</h2>
|
|
160
|
-
<p class="card-meta" id="sessionsMeta">awaiting…</p>
|
|
161
|
-
</div>
|
|
162
|
-
</header>
|
|
163
|
-
<div class="card-body card-body-flush">
|
|
164
|
-
<div class="table-scroll">
|
|
165
|
-
<table class="data" id="sessionsTable">
|
|
166
|
-
<thead>
|
|
167
|
-
<tr>
|
|
168
|
-
<th class="col-mark"></th>
|
|
169
|
-
<th>Title</th>
|
|
170
|
-
<th>Working directory</th>
|
|
171
|
-
<th class="num">Updated</th>
|
|
172
|
-
<th class="num">Started</th>
|
|
173
|
-
<th class="num">PID</th>
|
|
174
|
-
<th class="col-actions"></th>
|
|
175
|
-
</tr>
|
|
176
|
-
</thead>
|
|
177
|
-
<tbody></tbody>
|
|
178
|
-
</table>
|
|
179
|
-
</div>
|
|
180
|
-
<div class="empty" id="sessionsEmpty" hidden>No live sessions detected.</div>
|
|
181
|
-
</div>
|
|
182
|
-
</article>
|
|
183
|
-
|
|
184
|
-
<article class="card">
|
|
185
|
-
<header class="card-head">
|
|
186
|
-
<div class="card-titles">
|
|
187
|
-
<h2 class="card-title">Recently closed</h2>
|
|
188
|
-
<p class="card-meta" id="recentMeta">…</p>
|
|
189
|
-
</div>
|
|
190
|
-
</header>
|
|
191
|
-
<div class="card-body card-body-flush">
|
|
192
|
-
<div class="table-scroll">
|
|
193
|
-
<table class="data" id="recentTable">
|
|
194
|
-
<thead>
|
|
195
|
-
<tr>
|
|
196
|
-
<th>Title</th>
|
|
197
|
-
<th>Working directory</th>
|
|
198
|
-
<th>Branch</th>
|
|
199
|
-
<th class="num">Last activity</th>
|
|
200
|
-
<th class="num">Started</th>
|
|
201
|
-
<th class="col-actions"></th>
|
|
202
|
-
</tr>
|
|
203
|
-
</thead>
|
|
204
|
-
<tbody></tbody>
|
|
205
|
-
</table>
|
|
206
|
-
</div>
|
|
207
|
-
<div class="empty" id="recentEmpty" hidden>Nothing in <code>~/.claude/projects/</code>.</div>
|
|
208
|
-
<footer class="pagination" id="recentPagination" hidden>
|
|
209
|
-
<button class="action subtle small" id="recentPrevBtn" disabled>← Prev</button>
|
|
210
|
-
<span class="pagination-info">Page <strong id="recentPageNum">1</strong> of <strong id="recentPageTotal">1</strong> · <span id="recentTotal">0</span> total</span>
|
|
211
|
-
<button class="action subtle small" id="recentNextBtn" disabled>Next →</button>
|
|
212
|
-
<select id="recentPageSize" class="input" style="max-width: 100px;">
|
|
213
|
-
<option value="10">10 / page</option>
|
|
214
|
-
<option value="15" selected>15 / page</option>
|
|
215
|
-
<option value="25">25 / page</option>
|
|
216
|
-
<option value="50">50 / page</option>
|
|
217
|
-
</select>
|
|
218
|
-
</footer>
|
|
219
|
-
</div>
|
|
220
|
-
</article>
|
|
221
|
-
</section>
|
|
222
|
-
|
|
223
|
-
<!-- ─── Launch tab ─── -->
|
|
224
|
-
<section class="tab-panel" data-panel="launch">
|
|
225
|
-
<article class="card">
|
|
226
|
-
<header class="card-head">
|
|
227
|
-
<div class="card-titles">
|
|
228
|
-
<h2 class="card-title">New session</h2>
|
|
229
|
-
<p class="card-meta">Picks an unused workspace, clones missing repos, opens <code>claude</code> in a fresh terminal.</p>
|
|
230
|
-
</div>
|
|
231
|
-
</header>
|
|
232
|
-
<div class="card-body">
|
|
233
|
-
<div class="form-row">
|
|
234
|
-
<span class="form-label">Repos</span>
|
|
235
|
-
<div class="chip-row" id="repoPicker">
|
|
236
|
-
<span class="muted-text">no repos configured · add some in <strong>Configure</strong></span>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
<div class="form-row">
|
|
240
|
-
<label class="form-label" for="workspaceSelect">Workspace</label>
|
|
241
|
-
<select id="workspaceSelect" class="input narrow">
|
|
242
|
-
<option value="">auto — find or create unused</option>
|
|
243
|
-
</select>
|
|
244
|
-
<button class="action primary" id="newSessionBtn">Launch new session</button>
|
|
245
|
-
</div>
|
|
246
|
-
<div id="newSessionProgress" class="progress-list"></div>
|
|
247
|
-
<div class="post-result" id="newSessionResult"></div>
|
|
248
|
-
</div>
|
|
249
|
-
</article>
|
|
250
|
-
|
|
251
|
-
<article class="card">
|
|
252
|
-
<header class="card-head">
|
|
253
|
-
<div class="card-titles">
|
|
254
|
-
<h2 class="card-title">Snapshot & restore</h2>
|
|
255
|
-
<p class="card-meta" id="snapshotMeta">…</p>
|
|
256
|
-
</div>
|
|
257
|
-
</header>
|
|
258
|
-
<div class="card-body">
|
|
259
|
-
<div class="row gap-row">
|
|
260
|
-
<button class="action" id="snapshotSaveBtn">Save snapshot now</button>
|
|
261
|
-
<button class="action primary" id="snapshotRestoreBtn">Restore latest</button>
|
|
262
|
-
<span class="divider-dot">·</span>
|
|
263
|
-
<select id="historySelect" class="input narrow">
|
|
264
|
-
<option value="">history…</option>
|
|
265
|
-
</select>
|
|
266
|
-
<button class="action" id="historyRestoreBtn">Restore selected</button>
|
|
267
|
-
</div>
|
|
268
|
-
<details class="snapshot-detail">
|
|
269
|
-
<summary>View snapshot contents</summary>
|
|
270
|
-
<pre id="snapshotPreview" class="preview"></pre>
|
|
271
|
-
</details>
|
|
272
|
-
</div>
|
|
273
|
-
</article>
|
|
274
|
-
|
|
275
|
-
<article class="card">
|
|
276
|
-
<header class="card-head">
|
|
277
|
-
<div class="card-titles">
|
|
278
|
-
<h2 class="card-title">Workspaces on disk</h2>
|
|
279
|
-
<p class="card-meta">Under <code id="workDirDisplay">…</code></p>
|
|
280
|
-
</div>
|
|
281
|
-
</header>
|
|
282
|
-
<div class="card-body">
|
|
283
|
-
<div id="workspaceList" class="workspace-grid"></div>
|
|
284
|
-
</div>
|
|
285
|
-
</article>
|
|
286
|
-
</section>
|
|
287
|
-
|
|
288
|
-
<!-- ─── Configure tab ─── -->
|
|
289
|
-
<section class="tab-panel" data-panel="configure">
|
|
290
|
-
<article class="card">
|
|
291
|
-
<header class="card-head">
|
|
292
|
-
<div class="card-titles">
|
|
293
|
-
<h2 class="card-title">Settings</h2>
|
|
294
|
-
<p class="card-meta">Persisted to <code>~/.ccsm/config.json</code></p>
|
|
295
|
-
</div>
|
|
296
|
-
</header>
|
|
297
|
-
<div class="card-body">
|
|
298
|
-
<div class="config-grid">
|
|
299
|
-
<label class="field">
|
|
300
|
-
<span class="label">Port</span>
|
|
301
|
-
<input id="cfgPort" type="number" />
|
|
302
|
-
<span class="hint">restart server to apply</span>
|
|
303
|
-
</label>
|
|
304
|
-
<label class="field">
|
|
305
|
-
<span class="label">Work directory</span>
|
|
306
|
-
<input id="cfgWorkDir" type="text" />
|
|
307
|
-
</label>
|
|
308
|
-
<label class="field">
|
|
309
|
-
<span class="label">Snapshot interval (ms)</span>
|
|
310
|
-
<input id="cfgInterval" type="number" min="5000" />
|
|
311
|
-
</label>
|
|
312
|
-
<label class="field">
|
|
313
|
-
<span class="label">History kept</span>
|
|
314
|
-
<input id="cfgKeep" type="number" min="1" />
|
|
315
|
-
</label>
|
|
316
|
-
<label class="field">
|
|
317
|
-
<span class="label">Claude command</span>
|
|
318
|
-
<input id="cfgClaudeCommand" type="text" placeholder="claude" />
|
|
319
|
-
<span class="hint">alias / function / exe name</span>
|
|
320
|
-
</label>
|
|
321
|
-
<label class="field">
|
|
322
|
-
<span class="label">Terminal</span>
|
|
323
|
-
<select id="cfgTerminal" class="input"></select>
|
|
324
|
-
</label>
|
|
325
|
-
<label class="field">
|
|
326
|
-
<span class="label">Command shell <span class="hint inline">(wt only)</span></span>
|
|
327
|
-
<select id="cfgCommandShell" class="input">
|
|
328
|
-
<option value="pwsh">pwsh · PowerShell 7</option>
|
|
329
|
-
<option value="powershell">powershell · Windows PowerShell 5.1</option>
|
|
330
|
-
<option value="none">none · run command directly</option>
|
|
331
|
-
</select>
|
|
332
|
-
</label>
|
|
333
|
-
<label class="field">
|
|
334
|
-
<span class="label">Browser open mode</span>
|
|
335
|
-
<select id="cfgBrowserMode" class="input">
|
|
336
|
-
<option value="app">app · Edge/Chrome chromeless</option>
|
|
337
|
-
<option value="tab">tab · default browser</option>
|
|
338
|
-
<option value="none">off · don't open</option>
|
|
339
|
-
</select>
|
|
340
|
-
</label>
|
|
341
|
-
<label class="field toggle">
|
|
342
|
-
<input id="cfgAutoFocus" type="checkbox" />
|
|
343
|
-
<span class="toggle-text">
|
|
344
|
-
<span class="label">Auto-focus on launch</span>
|
|
345
|
-
<span class="hint">raise newly-launched terminal window</span>
|
|
346
|
-
</span>
|
|
347
|
-
</label>
|
|
348
|
-
<label class="field full">
|
|
349
|
-
<span class="label">Finder prompt</span>
|
|
350
|
-
<textarea id="cfgFinderPrompt" rows="3"></textarea>
|
|
351
|
-
<span class="hint">passed as initial prompt to the finder session</span>
|
|
352
|
-
</label>
|
|
353
|
-
|
|
354
|
-
<div class="field full">
|
|
355
|
-
<div class="repos-head">
|
|
356
|
-
<span class="label">Repositories</span>
|
|
357
|
-
<button class="action small" id="addRepoBtn">+ Add repo</button>
|
|
358
|
-
</div>
|
|
359
|
-
<table class="data repos-table" id="reposTable">
|
|
360
|
-
<thead>
|
|
361
|
-
<tr>
|
|
362
|
-
<th>Name</th>
|
|
363
|
-
<th>URL</th>
|
|
364
|
-
<th class="num">Default</th>
|
|
365
|
-
<th class="col-actions"></th>
|
|
366
|
-
</tr>
|
|
367
|
-
</thead>
|
|
368
|
-
<tbody></tbody>
|
|
369
|
-
</table>
|
|
370
|
-
</div>
|
|
371
|
-
|
|
372
|
-
<div class="form-actions full">
|
|
373
|
-
<button class="action primary" id="saveConfigBtn">Save configuration</button>
|
|
374
|
-
<span class="muted-text" id="configSavedAt"></span>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
</article>
|
|
379
|
-
</section>
|
|
380
|
-
</div>
|
|
381
|
-
|
|
382
|
-
<footer class="footer-status">
|
|
383
|
-
<span class="fs-key">Data</span> <span class="fs-val" id="footData">—</span>
|
|
384
|
-
<span class="fs-divider">·</span>
|
|
385
|
-
<span class="fs-key">Workspaces</span> <span class="fs-val" id="footWorkDir">—</span>
|
|
386
|
-
</footer>
|
|
387
|
-
</main>
|
|
388
|
-
</div>
|
|
389
|
-
|
|
390
|
-
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
391
|
-
|
|
392
|
-
<script src="/app.js" defer></script>
|
|
53
|
+
<div id="app"></div>
|
|
54
|
+
<script type="module" src="./js/main.js"></script>
|
|
55
|
+
<script>
|
|
56
|
+
// Dev hot-reload — only active when the page itself loads from a
|
|
57
|
+
// local backend (the /api/dev/ping endpoint exists only on a dev
|
|
58
|
+
// checkout). On the GH Pages copy this skips entirely.
|
|
59
|
+
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
60
|
+
fetch('/api/dev/ping', { cache: 'no-store' }).then((r) => {
|
|
61
|
+
if (!r.ok) return;
|
|
62
|
+
const es = new EventSource('/api/dev/reload');
|
|
63
|
+
es.addEventListener('reload', () => location.reload());
|
|
64
|
+
}).catch(() => {});
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
393
67
|
</body>
|
|
394
68
|
</html>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Mutation actions shared by SessionsPage, FavoritesTable etc. — each
|
|
2
|
+
// optimistically updates the relevant signal and rolls back on error.
|
|
3
|
+
|
|
4
|
+
import { favorites, labels, sessions, recent } from './state.js';
|
|
5
|
+
import { api, loadSessions, loadRecent } from './api.js';
|
|
6
|
+
import { setToast } from './toast.js';
|
|
7
|
+
import { ccsmPrompt } from './dialog.js';
|
|
8
|
+
|
|
9
|
+
export async function renameSession(sessionId, currentLabel) {
|
|
10
|
+
const next = await ccsmPrompt('Rename session', currentLabel || '', {
|
|
11
|
+
title: 'Rename session',
|
|
12
|
+
placeholder: 'leave empty to clear the label',
|
|
13
|
+
okLabel: 'Save',
|
|
14
|
+
});
|
|
15
|
+
if (next === null) return;
|
|
16
|
+
const trimmed = next.trim();
|
|
17
|
+
const prev = labels.value[sessionId];
|
|
18
|
+
const nextLabels = { ...labels.value };
|
|
19
|
+
if (trimmed) nextLabels[sessionId] = trimmed;
|
|
20
|
+
else delete nextLabels[sessionId];
|
|
21
|
+
labels.value = nextLabels;
|
|
22
|
+
try {
|
|
23
|
+
if (trimmed) {
|
|
24
|
+
await api('PUT', `/api/labels/${sessionId}`, { label: trimmed });
|
|
25
|
+
setToast(`renamed · ${sessionId.slice(0, 8)}`);
|
|
26
|
+
} else {
|
|
27
|
+
await api('DELETE', `/api/labels/${sessionId}`);
|
|
28
|
+
setToast(`cleared label · ${sessionId.slice(0, 8)}`);
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
const rollback = { ...labels.value };
|
|
32
|
+
if (prev !== undefined) rollback[sessionId] = prev;
|
|
33
|
+
else delete rollback[sessionId];
|
|
34
|
+
labels.value = rollback;
|
|
35
|
+
setToast('rename failed: ' + e.message, 'error');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// snapshotData: { cwd, title, gitBranch } — captured from the source row so
|
|
40
|
+
// the favorite stays renderable after the session leaves live/recent.
|
|
41
|
+
export async function toggleFavorite(sessionId, snapshotData = {}) {
|
|
42
|
+
const wasFav = !!favorites.value[sessionId];
|
|
43
|
+
if (wasFav) {
|
|
44
|
+
const next = { ...favorites.value };
|
|
45
|
+
delete next[sessionId];
|
|
46
|
+
favorites.value = next;
|
|
47
|
+
try { await api('DELETE', `/api/favorites/${sessionId}`); }
|
|
48
|
+
catch (e) { setToast('unfavorite failed: ' + e.message, 'error'); }
|
|
49
|
+
} else {
|
|
50
|
+
const { cwd = '', title = '', gitBranch = '' } = snapshotData;
|
|
51
|
+
favorites.value = {
|
|
52
|
+
...favorites.value,
|
|
53
|
+
[sessionId]: { sessionId, cwd, title, gitBranch, addedAt: Date.now() },
|
|
54
|
+
};
|
|
55
|
+
try { await api('POST', `/api/favorites/${sessionId}`, { cwd, title, gitBranch }); }
|
|
56
|
+
catch (e) { setToast('favorite failed: ' + e.message, 'error'); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function focusSession(sessionId) {
|
|
61
|
+
try {
|
|
62
|
+
const r = await api('POST', `/api/sessions/${sessionId}/focus`);
|
|
63
|
+
if (r.ok && r.activated) setToast(`focused · ${r.windowTitle || sessionId.slice(0, 8)}`);
|
|
64
|
+
else if (r.ok) setToast(`window found, focus blocked (${r.windowProcess})`, 'error');
|
|
65
|
+
else setToast(`no window for pid · ${(r.chain || []).map((c) => c.name).join('→')}`, 'error');
|
|
66
|
+
} catch (e) { setToast(e.message, 'error'); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function resumeSession(sessionId, cwd, { kind = 'resume' } = {}) {
|
|
70
|
+
if (!cwd) return setToast('no cwd for this session', 'error');
|
|
71
|
+
try {
|
|
72
|
+
await api('POST', `/api/sessions/${sessionId}/resume`, { cwd });
|
|
73
|
+
const verb = kind === 'continue' ? 'continuing' : 'opening wt';
|
|
74
|
+
setToast(`${verb} · ${sessionId.slice(0, 8)}…`);
|
|
75
|
+
if (kind === 'continue') {
|
|
76
|
+
setTimeout(() => loadSessions().catch(() => {}), 3000);
|
|
77
|
+
setTimeout(() => loadRecent().catch(() => {}), 4000);
|
|
78
|
+
}
|
|
79
|
+
} catch (e) { setToast(e.message, 'error'); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runFinder() {
|
|
83
|
+
try {
|
|
84
|
+
await api('POST', '/api/sessions/finder');
|
|
85
|
+
setToast('finder session launching in a new wt window');
|
|
86
|
+
} catch (e) { setToast(e.message, 'error'); }
|
|
87
|
+
}
|
package/public/js/api.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Fetch wrapper + every loader. Loaders push into signals from ./state.js.
|
|
2
|
+
// Cross-origin (hosted frontend → local backend) flows through httpBase().
|
|
3
|
+
|
|
4
|
+
import * as S from './state.js';
|
|
5
|
+
import { httpBase } from './backend.js';
|
|
6
|
+
|
|
7
|
+
export async function api(method, url, body) {
|
|
8
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
9
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
10
|
+
const r = await fetch(httpBase() + url, opts);
|
|
11
|
+
const text = await r.text();
|
|
12
|
+
let json;
|
|
13
|
+
try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
|
|
14
|
+
if (!r.ok) throw new Error(json.error || `HTTP ${r.status}`);
|
|
15
|
+
return json;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadConfig() {
|
|
19
|
+
const [cfg, terms, caps] = await Promise.all([
|
|
20
|
+
api('GET', '/api/config'),
|
|
21
|
+
api('GET', '/api/terminals'),
|
|
22
|
+
api('GET', '/api/capabilities').catch(() => ({ webTerminal: false })),
|
|
23
|
+
]);
|
|
24
|
+
S.config.value = cfg;
|
|
25
|
+
S.terminals.value = terms.terminals;
|
|
26
|
+
S.capabilities.value = caps;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function loadWebTerminals() {
|
|
30
|
+
try {
|
|
31
|
+
const r = await api('GET', '/api/sessions/web');
|
|
32
|
+
S.webTerminals.value = r.terminals || [];
|
|
33
|
+
} catch { /* node-pty might be unavailable */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function killWebTerminal(id) {
|
|
37
|
+
await api('DELETE', `/api/sessions/web/${id}`);
|
|
38
|
+
await loadWebTerminals();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function loadSessions() {
|
|
42
|
+
const r = await api('GET', '/api/sessions');
|
|
43
|
+
S.sessions.value = r.sessions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadRecent() {
|
|
47
|
+
const r = await api('GET', `/api/sessions/recent?limit=${S.recentLimit.value}&offset=${S.recentOffset.value}`);
|
|
48
|
+
S.recent.value = r.recent;
|
|
49
|
+
S.recentTotal.value = r.total || 0;
|
|
50
|
+
S.recentLimit.value = r.limit || S.recentLimit.value;
|
|
51
|
+
S.recentOffset.value = r.offset || 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function loadFavorites() {
|
|
55
|
+
try {
|
|
56
|
+
const r = await api('GET', '/api/favorites');
|
|
57
|
+
const map = {};
|
|
58
|
+
for (const f of r.favorites || []) map[f.sessionId] = f;
|
|
59
|
+
S.favorites.value = map;
|
|
60
|
+
} catch (e) { /* ignore — endpoint may not exist on older servers */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function loadLabels() {
|
|
64
|
+
try {
|
|
65
|
+
const r = await api('GET', '/api/labels');
|
|
66
|
+
S.labels.value = r.labels || {};
|
|
67
|
+
} catch (e) { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function loadSnapshot() {
|
|
71
|
+
const r = await api('GET', '/api/snapshot');
|
|
72
|
+
S.snapshot.value = r.snapshot;
|
|
73
|
+
const h = await api('GET', '/api/snapshot/history');
|
|
74
|
+
S.history.value = h.history;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function loadWorkspaces() {
|
|
78
|
+
const r = await api('GET', '/api/workspaces');
|
|
79
|
+
S.workspaces.value = r.workspaces;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function refreshAll() {
|
|
83
|
+
await Promise.all([
|
|
84
|
+
loadSessions(), loadRecent(), loadSnapshot(),
|
|
85
|
+
loadWorkspaces(), loadFavorites(), loadLabels(), loadWebTerminals(),
|
|
86
|
+
]);
|
|
87
|
+
S.lastRefreshAt.value = Date.now();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function pollHealth() {
|
|
91
|
+
const ctrl = new AbortController();
|
|
92
|
+
const t = setTimeout(() => ctrl.abort(), 3000);
|
|
93
|
+
try {
|
|
94
|
+
const r = await fetch(httpBase() + '/api/health', { signal: ctrl.signal });
|
|
95
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
96
|
+
const j = await r.json();
|
|
97
|
+
S.serverHealth.value = { state: 'online', version: j.version, pid: j.pid };
|
|
98
|
+
} catch (e) {
|
|
99
|
+
S.serverHealth.value = { state: 'offline', error: String(e.message || e) };
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(t);
|
|
102
|
+
}
|
|
103
|
+
}
|