python-codex 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
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.
- pycodex/cli.py +21 -13
- pycodex/context.py +4 -1
- pycodex/feishu_link.py +32 -14
- {python_codex-0.2.2.dist-info → python_codex-0.2.4.dist-info}/METADATA +21 -4
- {python_codex-0.2.2.dist-info → python_codex-0.2.4.dist-info}/RECORD +13 -11
- workspace_server/__init__.py +19 -3
- workspace_server/app.py +546 -373
- workspace_server/workspace.html +53 -3
- workspace_server/workspaces.html +551 -0
- workspace_server/workspaces.py +561 -0
- {python_codex-0.2.2.dist-info → python_codex-0.2.4.dist-info}/WHEEL +0 -0
- {python_codex-0.2.2.dist-info → python_codex-0.2.4.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.2.dist-info → python_codex-0.2.4.dist-info}/licenses/LICENSE +0 -0
workspace_server/workspace.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>
|
|
6
|
+
<title>__WORKSPACE_TITLE__</title>
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
9
9
|
color-scheme: light;
|
|
@@ -63,13 +63,57 @@
|
|
|
63
63
|
.boardbar {
|
|
64
64
|
min-width: 0;
|
|
65
65
|
border-bottom: 1px solid var(--line);
|
|
66
|
-
padding:
|
|
66
|
+
padding: 0;
|
|
67
67
|
color: var(--muted);
|
|
68
68
|
font-size: 12px;
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--space-5);
|
|
69
72
|
white-space: nowrap;
|
|
70
73
|
overflow: hidden;
|
|
74
|
+
}
|
|
75
|
+
.boardbar-paths {
|
|
76
|
+
min-width: 0;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--space-4);
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
line-height: 20px;
|
|
82
|
+
}
|
|
83
|
+
.boardbar-paths span {
|
|
84
|
+
min-width: 0;
|
|
85
|
+
overflow: hidden;
|
|
71
86
|
text-overflow: ellipsis;
|
|
72
87
|
}
|
|
88
|
+
.workspace-back {
|
|
89
|
+
flex: 0 0 auto;
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
width: 28px;
|
|
94
|
+
height: 20px;
|
|
95
|
+
border: 0;
|
|
96
|
+
border-radius: var(--radius-sm);
|
|
97
|
+
background: transparent;
|
|
98
|
+
color: var(--ink);
|
|
99
|
+
padding: 0;
|
|
100
|
+
text-decoration: none;
|
|
101
|
+
}
|
|
102
|
+
.workspace-back:hover {
|
|
103
|
+
background: #eef7f1;
|
|
104
|
+
}
|
|
105
|
+
.workspace-back:focus {
|
|
106
|
+
outline: 2px solid #9bb7a6;
|
|
107
|
+
outline-offset: 1px;
|
|
108
|
+
}
|
|
109
|
+
.workspace-back::before {
|
|
110
|
+
content: "";
|
|
111
|
+
width: 7px;
|
|
112
|
+
height: 7px;
|
|
113
|
+
border-left: 2px solid currentColor;
|
|
114
|
+
border-bottom: 2px solid currentColor;
|
|
115
|
+
transform: rotate(45deg);
|
|
116
|
+
}
|
|
73
117
|
.board {
|
|
74
118
|
border: 0;
|
|
75
119
|
min-width: 0;
|
|
@@ -563,7 +607,13 @@
|
|
|
563
607
|
<body>
|
|
564
608
|
<div class="workspace">
|
|
565
609
|
<section class="board-pane" aria-label="workspace board">
|
|
566
|
-
<div class="boardbar">
|
|
610
|
+
<div class="boardbar">
|
|
611
|
+
<a class="workspace-back" href="/" title="Back to workspaces" aria-label="Back to workspaces"></a>
|
|
612
|
+
<div class="boardbar-paths">
|
|
613
|
+
<span>@<code>__CWD_LABEL__</code></span>
|
|
614
|
+
<span>Board: <code>__BOARD_LABEL__</code></span>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
567
617
|
<iframe id="boardFrame" class="board" src="board" title="board"></iframe>
|
|
568
618
|
</section>
|
|
569
619
|
<button id="splitter" class="splitter" type="button" aria-label="Resize chat panel"></button>
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>pycodex workspaces</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
--bg: #f6f7f9;
|
|
11
|
+
--panel: #ffffff;
|
|
12
|
+
--panel-soft: #f0f3f6;
|
|
13
|
+
--text: #17202a;
|
|
14
|
+
--muted: #687483;
|
|
15
|
+
--line: #dbe1e8;
|
|
16
|
+
--accent: #0b6bcb;
|
|
17
|
+
--accent-strong: #0859a8;
|
|
18
|
+
--danger: #b42318;
|
|
19
|
+
--danger-soft: #fff0ed;
|
|
20
|
+
--shadow: 0 14px 36px rgba(24, 39, 75, .08);
|
|
21
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
22
|
+
}
|
|
23
|
+
@media (prefers-color-scheme: dark) {
|
|
24
|
+
:root {
|
|
25
|
+
--bg: #101418;
|
|
26
|
+
--panel: #171d23;
|
|
27
|
+
--panel-soft: #202832;
|
|
28
|
+
--text: #eef3f8;
|
|
29
|
+
--muted: #9aa7b5;
|
|
30
|
+
--line: #2d3844;
|
|
31
|
+
--accent: #5aa9ff;
|
|
32
|
+
--accent-strong: #83bdff;
|
|
33
|
+
--danger: #ff8c7f;
|
|
34
|
+
--danger-soft: #321d1b;
|
|
35
|
+
--shadow: 0 14px 36px rgba(0, 0, 0, .28);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
* {
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
}
|
|
41
|
+
body {
|
|
42
|
+
margin: 0;
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
background: var(--bg);
|
|
45
|
+
color: var(--text);
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
}
|
|
48
|
+
button, input {
|
|
49
|
+
font: inherit;
|
|
50
|
+
}
|
|
51
|
+
button, a.button {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
min-height: 34px;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
gap: 7px;
|
|
57
|
+
border: 1px solid var(--line);
|
|
58
|
+
border-radius: 7px;
|
|
59
|
+
padding: 7px 11px;
|
|
60
|
+
background: var(--panel);
|
|
61
|
+
color: var(--text);
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
}
|
|
66
|
+
button.primary {
|
|
67
|
+
border-color: var(--accent);
|
|
68
|
+
background: var(--accent);
|
|
69
|
+
color: #fff;
|
|
70
|
+
}
|
|
71
|
+
button.primary:hover {
|
|
72
|
+
background: var(--accent-strong);
|
|
73
|
+
}
|
|
74
|
+
button.danger {
|
|
75
|
+
border-color: color-mix(in srgb, var(--danger) 30%, var(--line) 70%);
|
|
76
|
+
background: var(--danger-soft);
|
|
77
|
+
color: var(--danger);
|
|
78
|
+
}
|
|
79
|
+
button:disabled {
|
|
80
|
+
cursor: default;
|
|
81
|
+
opacity: .62;
|
|
82
|
+
}
|
|
83
|
+
input {
|
|
84
|
+
width: 100%;
|
|
85
|
+
min-height: 38px;
|
|
86
|
+
border: 1px solid var(--line);
|
|
87
|
+
border-radius: 7px;
|
|
88
|
+
padding: 8px 10px;
|
|
89
|
+
background: var(--panel);
|
|
90
|
+
color: var(--text);
|
|
91
|
+
outline: none;
|
|
92
|
+
}
|
|
93
|
+
input:focus {
|
|
94
|
+
border-color: var(--accent);
|
|
95
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
|
96
|
+
}
|
|
97
|
+
input::placeholder {
|
|
98
|
+
color: color-mix(in srgb, var(--muted) 45%, transparent);
|
|
99
|
+
}
|
|
100
|
+
.page {
|
|
101
|
+
width: min(1180px, calc(100vw - 36px));
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
padding: 24px 0 42px;
|
|
104
|
+
}
|
|
105
|
+
.topbar {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
gap: 16px;
|
|
110
|
+
margin-bottom: 22px;
|
|
111
|
+
}
|
|
112
|
+
.brand {
|
|
113
|
+
min-width: 0;
|
|
114
|
+
}
|
|
115
|
+
.brand h1 {
|
|
116
|
+
margin: 0;
|
|
117
|
+
font-size: 24px;
|
|
118
|
+
line-height: 1.15;
|
|
119
|
+
font-weight: 720;
|
|
120
|
+
letter-spacing: 0;
|
|
121
|
+
}
|
|
122
|
+
.brand p {
|
|
123
|
+
margin: 5px 0 0;
|
|
124
|
+
color: var(--muted);
|
|
125
|
+
line-height: 1.45;
|
|
126
|
+
}
|
|
127
|
+
.status {
|
|
128
|
+
min-width: 220px;
|
|
129
|
+
max-width: 420px;
|
|
130
|
+
color: var(--muted);
|
|
131
|
+
text-align: right;
|
|
132
|
+
line-height: 1.4;
|
|
133
|
+
}
|
|
134
|
+
.status.error {
|
|
135
|
+
color: var(--danger);
|
|
136
|
+
}
|
|
137
|
+
.layout {
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
|
140
|
+
gap: 18px;
|
|
141
|
+
align-items: start;
|
|
142
|
+
}
|
|
143
|
+
.panel {
|
|
144
|
+
border: 1px solid var(--line);
|
|
145
|
+
border-radius: 8px;
|
|
146
|
+
background: var(--panel);
|
|
147
|
+
box-shadow: var(--shadow);
|
|
148
|
+
}
|
|
149
|
+
.panel-head {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
gap: 12px;
|
|
154
|
+
border-bottom: 1px solid var(--line);
|
|
155
|
+
padding: 15px 16px;
|
|
156
|
+
}
|
|
157
|
+
.panel-title {
|
|
158
|
+
margin: 0;
|
|
159
|
+
font-size: 15px;
|
|
160
|
+
font-weight: 680;
|
|
161
|
+
}
|
|
162
|
+
.panel-subtitle {
|
|
163
|
+
margin: 3px 0 0;
|
|
164
|
+
color: var(--muted);
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
line-height: 1.35;
|
|
167
|
+
}
|
|
168
|
+
form {
|
|
169
|
+
display: grid;
|
|
170
|
+
gap: 13px;
|
|
171
|
+
padding: 16px;
|
|
172
|
+
}
|
|
173
|
+
label {
|
|
174
|
+
display: grid;
|
|
175
|
+
gap: 6px;
|
|
176
|
+
color: var(--muted);
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
font-weight: 620;
|
|
179
|
+
}
|
|
180
|
+
label span {
|
|
181
|
+
color: var(--muted);
|
|
182
|
+
}
|
|
183
|
+
.form-actions {
|
|
184
|
+
display: flex;
|
|
185
|
+
gap: 9px;
|
|
186
|
+
align-items: center;
|
|
187
|
+
justify-content: flex-end;
|
|
188
|
+
margin-top: 2px;
|
|
189
|
+
}
|
|
190
|
+
.summary {
|
|
191
|
+
display: grid;
|
|
192
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
193
|
+
gap: 10px;
|
|
194
|
+
padding: 16px;
|
|
195
|
+
border-bottom: 1px solid var(--line);
|
|
196
|
+
background: var(--panel-soft);
|
|
197
|
+
}
|
|
198
|
+
.metric {
|
|
199
|
+
min-width: 0;
|
|
200
|
+
border: 1px solid var(--line);
|
|
201
|
+
border-radius: 8px;
|
|
202
|
+
padding: 10px;
|
|
203
|
+
background: var(--panel);
|
|
204
|
+
}
|
|
205
|
+
.metric-value {
|
|
206
|
+
display: block;
|
|
207
|
+
font-size: 20px;
|
|
208
|
+
font-weight: 720;
|
|
209
|
+
line-height: 1.1;
|
|
210
|
+
}
|
|
211
|
+
.metric-label {
|
|
212
|
+
display: block;
|
|
213
|
+
margin-top: 4px;
|
|
214
|
+
color: var(--muted);
|
|
215
|
+
font-size: 12px;
|
|
216
|
+
}
|
|
217
|
+
.workspace-list {
|
|
218
|
+
display: grid;
|
|
219
|
+
gap: 10px;
|
|
220
|
+
padding: 14px;
|
|
221
|
+
}
|
|
222
|
+
.workspace-row {
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-columns: minmax(150px, 220px) minmax(0, 1fr) auto;
|
|
225
|
+
gap: 12px;
|
|
226
|
+
align-items: center;
|
|
227
|
+
border: 1px solid var(--line);
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
padding: 12px;
|
|
230
|
+
background: var(--panel);
|
|
231
|
+
}
|
|
232
|
+
.workspace-name {
|
|
233
|
+
min-width: 0;
|
|
234
|
+
}
|
|
235
|
+
.workspace-name strong {
|
|
236
|
+
display: block;
|
|
237
|
+
overflow: hidden;
|
|
238
|
+
text-overflow: ellipsis;
|
|
239
|
+
white-space: nowrap;
|
|
240
|
+
font-size: 15px;
|
|
241
|
+
}
|
|
242
|
+
.session-count {
|
|
243
|
+
display: inline-flex;
|
|
244
|
+
margin-top: 5px;
|
|
245
|
+
border-radius: 999px;
|
|
246
|
+
padding: 2px 8px;
|
|
247
|
+
background: var(--panel-soft);
|
|
248
|
+
color: var(--muted);
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
}
|
|
251
|
+
.paths {
|
|
252
|
+
display: grid;
|
|
253
|
+
gap: 6px;
|
|
254
|
+
min-width: 0;
|
|
255
|
+
}
|
|
256
|
+
.path-line {
|
|
257
|
+
display: grid;
|
|
258
|
+
grid-template-columns: 64px minmax(0, 1fr);
|
|
259
|
+
gap: 8px;
|
|
260
|
+
align-items: baseline;
|
|
261
|
+
min-width: 0;
|
|
262
|
+
}
|
|
263
|
+
.path-label {
|
|
264
|
+
color: var(--muted);
|
|
265
|
+
font-size: 12px;
|
|
266
|
+
font-weight: 620;
|
|
267
|
+
}
|
|
268
|
+
.path-value {
|
|
269
|
+
overflow: hidden;
|
|
270
|
+
text-overflow: ellipsis;
|
|
271
|
+
white-space: nowrap;
|
|
272
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
}
|
|
275
|
+
.actions {
|
|
276
|
+
display: flex;
|
|
277
|
+
gap: 8px;
|
|
278
|
+
justify-content: flex-end;
|
|
279
|
+
flex-wrap: wrap;
|
|
280
|
+
}
|
|
281
|
+
.empty {
|
|
282
|
+
display: grid;
|
|
283
|
+
justify-items: center;
|
|
284
|
+
gap: 8px;
|
|
285
|
+
padding: 54px 18px;
|
|
286
|
+
color: var(--muted);
|
|
287
|
+
text-align: center;
|
|
288
|
+
}
|
|
289
|
+
.empty strong {
|
|
290
|
+
color: var(--text);
|
|
291
|
+
font-size: 15px;
|
|
292
|
+
}
|
|
293
|
+
@media (max-width: 860px) {
|
|
294
|
+
.page {
|
|
295
|
+
width: min(100vw - 24px, 720px);
|
|
296
|
+
padding-top: 18px;
|
|
297
|
+
}
|
|
298
|
+
.topbar {
|
|
299
|
+
align-items: flex-start;
|
|
300
|
+
flex-direction: column;
|
|
301
|
+
}
|
|
302
|
+
.status {
|
|
303
|
+
min-width: 0;
|
|
304
|
+
text-align: left;
|
|
305
|
+
}
|
|
306
|
+
.layout {
|
|
307
|
+
grid-template-columns: 1fr;
|
|
308
|
+
}
|
|
309
|
+
.workspace-row {
|
|
310
|
+
grid-template-columns: 1fr;
|
|
311
|
+
align-items: stretch;
|
|
312
|
+
}
|
|
313
|
+
.actions {
|
|
314
|
+
justify-content: flex-start;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
@media (max-width: 520px) {
|
|
318
|
+
.summary {
|
|
319
|
+
grid-template-columns: 1fr;
|
|
320
|
+
}
|
|
321
|
+
.path-line {
|
|
322
|
+
grid-template-columns: 1fr;
|
|
323
|
+
}
|
|
324
|
+
.path-value {
|
|
325
|
+
white-space: normal;
|
|
326
|
+
overflow-wrap: anywhere;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
</style>
|
|
330
|
+
</head>
|
|
331
|
+
<body>
|
|
332
|
+
<main class="page">
|
|
333
|
+
<header class="topbar">
|
|
334
|
+
<div class="brand">
|
|
335
|
+
<h1>pycodex workspaces</h1>
|
|
336
|
+
<p>Manage local boards and working directories served by this process.</p>
|
|
337
|
+
</div>
|
|
338
|
+
<div id="status" class="status" role="status"></div>
|
|
339
|
+
</header>
|
|
340
|
+
|
|
341
|
+
<div class="layout">
|
|
342
|
+
<section class="panel" aria-labelledby="addTitle">
|
|
343
|
+
<div class="panel-head">
|
|
344
|
+
<div>
|
|
345
|
+
<h2 id="addTitle" class="panel-title">Add Workspace</h2>
|
|
346
|
+
<p class="panel-subtitle">Board identifies the workspace. Name is optional.</p>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<form id="workspaceForm">
|
|
350
|
+
<label>Board
|
|
351
|
+
<input id="boardInput" name="board" placeholder="/tmp/pcws-xxxx.html" spellcheck="false">
|
|
352
|
+
</label>
|
|
353
|
+
<label>Directory
|
|
354
|
+
<input id="dirInput" name="dir" placeholder="./" spellcheck="false">
|
|
355
|
+
</label>
|
|
356
|
+
<label>Name <span>optional</span>
|
|
357
|
+
<input id="nameInput" name="name" autocomplete="off" placeholder="workspace-1" spellcheck="false">
|
|
358
|
+
</label>
|
|
359
|
+
<div class="form-actions">
|
|
360
|
+
<button id="addButton" class="primary" type="submit">Add</button>
|
|
361
|
+
</div>
|
|
362
|
+
</form>
|
|
363
|
+
</section>
|
|
364
|
+
|
|
365
|
+
<section class="panel" aria-labelledby="listTitle">
|
|
366
|
+
<div class="panel-head">
|
|
367
|
+
<div>
|
|
368
|
+
<h2 id="listTitle" class="panel-title">Workspace Overview</h2>
|
|
369
|
+
<p class="panel-subtitle">Open a board, inspect paths, or remove a workspace from this process.</p>
|
|
370
|
+
</div>
|
|
371
|
+
<button id="refreshButton" type="button">Refresh</button>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="summary" aria-label="Workspace summary">
|
|
374
|
+
<div class="metric">
|
|
375
|
+
<span id="workspaceCount" class="metric-value">0</span>
|
|
376
|
+
<span class="metric-label">workspaces</span>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="metric">
|
|
379
|
+
<span id="sessionCount" class="metric-value">0</span>
|
|
380
|
+
<span class="metric-label">sessions</span>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="metric">
|
|
383
|
+
<span id="activeCount" class="metric-value">0</span>
|
|
384
|
+
<span class="metric-label">running</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div id="workspaceRows" class="workspace-list"></div>
|
|
388
|
+
</section>
|
|
389
|
+
</div>
|
|
390
|
+
</main>
|
|
391
|
+
|
|
392
|
+
<script>
|
|
393
|
+
const rows = document.getElementById("workspaceRows");
|
|
394
|
+
const form = document.getElementById("workspaceForm");
|
|
395
|
+
const statusEl = document.getElementById("status");
|
|
396
|
+
const nameInput = document.getElementById("nameInput");
|
|
397
|
+
const dirInput = document.getElementById("dirInput");
|
|
398
|
+
const boardInput = document.getElementById("boardInput");
|
|
399
|
+
const addButton = document.getElementById("addButton");
|
|
400
|
+
const refreshButton = document.getElementById("refreshButton");
|
|
401
|
+
const workspaceCount = document.getElementById("workspaceCount");
|
|
402
|
+
const sessionCount = document.getElementById("sessionCount");
|
|
403
|
+
const activeCount = document.getElementById("activeCount");
|
|
404
|
+
|
|
405
|
+
function api(path) {
|
|
406
|
+
return new URL(path, window.location.href).toString();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function setStatus(text, isError = false) {
|
|
410
|
+
statusEl.textContent = text || "";
|
|
411
|
+
statusEl.classList.toggle("error", Boolean(isError));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function textNode(tag, className, text) {
|
|
415
|
+
const node = document.createElement(tag);
|
|
416
|
+
node.className = className;
|
|
417
|
+
node.textContent = text || "";
|
|
418
|
+
return node;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function pathLine(label, value) {
|
|
422
|
+
const row = document.createElement("div");
|
|
423
|
+
row.className = "path-line";
|
|
424
|
+
row.appendChild(textNode("span", "path-label", label));
|
|
425
|
+
const path = textNode("span", "path-value", value || "");
|
|
426
|
+
path.title = value || "";
|
|
427
|
+
row.appendChild(path);
|
|
428
|
+
return row;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderSummary(workspaces) {
|
|
432
|
+
const sessions = [];
|
|
433
|
+
workspaces.forEach((workspace) => {
|
|
434
|
+
(workspace.sessions || []).forEach((session) => sessions.push(session));
|
|
435
|
+
});
|
|
436
|
+
workspaceCount.textContent = String(workspaces.length);
|
|
437
|
+
sessionCount.textContent = String(sessions.length);
|
|
438
|
+
activeCount.textContent = String(sessions.filter((session) => session.running).length);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function renderEmpty() {
|
|
442
|
+
const empty = document.createElement("div");
|
|
443
|
+
empty.className = "empty";
|
|
444
|
+
empty.appendChild(textNode("strong", "", "No workspaces yet"));
|
|
445
|
+
empty.appendChild(textNode("span", "", "Add one to open a board and start a pycodex session."));
|
|
446
|
+
rows.appendChild(empty);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function renderWorkspace(workspace) {
|
|
450
|
+
const row = document.createElement("article");
|
|
451
|
+
row.className = "workspace-row";
|
|
452
|
+
|
|
453
|
+
const name = document.createElement("div");
|
|
454
|
+
name.className = "workspace-name";
|
|
455
|
+
name.appendChild(textNode("strong", "", workspace.id || ""));
|
|
456
|
+
const sessions = Array.isArray(workspace.sessions) ? workspace.sessions : [];
|
|
457
|
+
name.appendChild(textNode("span", "session-count", `${sessions.length} session${sessions.length === 1 ? "" : "s"}`));
|
|
458
|
+
|
|
459
|
+
const paths = document.createElement("div");
|
|
460
|
+
paths.className = "paths";
|
|
461
|
+
paths.appendChild(pathLine("Directory", workspace.work_dir || ""));
|
|
462
|
+
paths.appendChild(pathLine("Board", workspace.board_path || ""));
|
|
463
|
+
|
|
464
|
+
const actions = document.createElement("div");
|
|
465
|
+
actions.className = "actions";
|
|
466
|
+
const open = document.createElement("a");
|
|
467
|
+
open.className = "button";
|
|
468
|
+
open.href = `w/${encodeURIComponent(workspace.id || "")}/`;
|
|
469
|
+
open.textContent = "Open";
|
|
470
|
+
const remove = document.createElement("button");
|
|
471
|
+
remove.className = "danger";
|
|
472
|
+
remove.type = "button";
|
|
473
|
+
remove.textContent = "Delete";
|
|
474
|
+
remove.addEventListener("click", () => deleteWorkspace(workspace.id || ""));
|
|
475
|
+
actions.appendChild(open);
|
|
476
|
+
actions.appendChild(remove);
|
|
477
|
+
|
|
478
|
+
row.appendChild(name);
|
|
479
|
+
row.appendChild(paths);
|
|
480
|
+
row.appendChild(actions);
|
|
481
|
+
rows.appendChild(row);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function render(workspaces) {
|
|
485
|
+
rows.textContent = "";
|
|
486
|
+
renderSummary(workspaces);
|
|
487
|
+
if (!workspaces.length) {
|
|
488
|
+
renderEmpty();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
workspaces.forEach(renderWorkspace);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function refresh() {
|
|
495
|
+
setStatus("Refreshing...");
|
|
496
|
+
const response = await fetch(api("api/workspaces"));
|
|
497
|
+
const payload = await response.json();
|
|
498
|
+
if (!response.ok) {
|
|
499
|
+
throw new Error(payload.error || `HTTP ${response.status}`);
|
|
500
|
+
}
|
|
501
|
+
render(Array.isArray(payload.workspaces) ? payload.workspaces : []);
|
|
502
|
+
setStatus("");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function addWorkspace(event) {
|
|
506
|
+
event.preventDefault();
|
|
507
|
+
setStatus("");
|
|
508
|
+
addButton.disabled = true;
|
|
509
|
+
const response = await fetch(api("api/workspaces"), {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: {"Content-Type": "application/json"},
|
|
512
|
+
body: JSON.stringify({
|
|
513
|
+
name: nameInput.value.trim(),
|
|
514
|
+
dir: dirInput.value.trim() || "./",
|
|
515
|
+
board: boardInput.value.trim() || null,
|
|
516
|
+
}),
|
|
517
|
+
});
|
|
518
|
+
const payload = await response.json();
|
|
519
|
+
addButton.disabled = false;
|
|
520
|
+
if (!response.ok || !payload.ok) {
|
|
521
|
+
setStatus(payload.error || `HTTP ${response.status}`, true);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
nameInput.value = "";
|
|
525
|
+
dirInput.value = "";
|
|
526
|
+
boardInput.value = "";
|
|
527
|
+
render(payload.workspaces || []);
|
|
528
|
+
setStatus(`Added ${(payload.workspace && payload.workspace.id) || "workspace"}.`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function deleteWorkspace(name) {
|
|
532
|
+
if (!name) return;
|
|
533
|
+
setStatus("");
|
|
534
|
+
const response = await fetch(api(`api/workspaces/${encodeURIComponent(name)}`), {
|
|
535
|
+
method: "DELETE",
|
|
536
|
+
});
|
|
537
|
+
const payload = await response.json();
|
|
538
|
+
if (!response.ok || !payload.ok) {
|
|
539
|
+
setStatus(payload.error || `HTTP ${response.status}`, true);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
render(payload.workspaces || []);
|
|
543
|
+
setStatus(`Deleted ${name}.`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
form.addEventListener("submit", addWorkspace);
|
|
547
|
+
refreshButton.addEventListener("click", () => refresh().catch((error) => setStatus(error.message, true)));
|
|
548
|
+
refresh().catch((error) => setStatus(error.message, true));
|
|
549
|
+
</script>
|
|
550
|
+
</body>
|
|
551
|
+
</html>
|