bosun 0.28.0 → 0.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +22 -1
- package/README.md +16 -5
- package/agent-pool.mjs +16 -2
- package/agent-work-analyzer.mjs +419 -0
- package/claude-shell.mjs +12 -2
- package/desktop/launch.mjs +31 -2
- package/desktop/main.mjs +132 -10
- package/desktop/package.json +8 -0
- package/desktop-shortcut.mjs +20 -1
- package/monitor.mjs +151 -0
- package/package.json +3 -2
- package/rotate-agent-logs.sh +8 -7
- package/setup.mjs +759 -59
- package/task-executor.mjs +309 -12
- package/telegram-bot.mjs +419 -5
- package/ui/app.js +2 -0
- package/ui/components/shared.js +31 -2
- package/ui/demo.html +8 -0
- package/ui/modules/icons.js +14 -0
- package/ui/modules/router.js +1 -0
- package/ui/modules/settings-schema.js +11 -1
- package/ui/modules/state.js +59 -0
- package/ui/styles/components.css +60 -8
- package/ui/styles.css +88 -0
- package/ui/tabs/tasks.js +110 -72
- package/ui/tabs/telemetry.js +167 -0
- package/ui-server.mjs +158 -0
- package/workspace-manager.mjs +45 -1
- package/ui/styles/workspace-switcher.css.bak +0 -693
|
@@ -53,6 +53,7 @@ export const SETTINGS_SCHEMA = [
|
|
|
53
53
|
{ key: "TELEGRAM_API_BASE_URL", label: "API Base URL", category: "telegram", type: "string", defaultVal: "https://api.telegram.org", description: "Override for Telegram API proxy.", advanced: true, validate: "^https?://" },
|
|
54
54
|
{ key: "TELEGRAM_HTTP_TIMEOUT_MS", label: "HTTP Timeout", category: "telegram", type: "number", defaultVal: 15000, min: 5000, max: 60000, unit: "ms", description: "Per-request timeout for Telegram API calls.", advanced: true },
|
|
55
55
|
{ key: "TELEGRAM_RETRY_ATTEMPTS", label: "Retry Attempts", category: "telegram", type: "number", defaultVal: 4, min: 0, max: 10, description: "Number of retry attempts for transient Telegram API failures.", advanced: true },
|
|
56
|
+
{ key: "TELEGRAM_HISTORY_RETENTION_DAYS", label: "Auto-Delete History", category: "telegram", type: "number", defaultVal: 3, min: 0, max: 365, unit: "days", description: "Automatically delete bot messages older than this many days to keep the chat tidy. 0 = disabled. Note: Telegram may silently skip messages older than 48 h in private chats.", restart: false },
|
|
56
57
|
{ key: "PROJECT_NAME", label: "Project Name", category: "telegram", type: "string", description: "Display name used in Telegram messages and logs. Auto-detected from package.json if not set." },
|
|
57
58
|
|
|
58
59
|
// ── Mini App / UI Server ──────────────────────────────────────
|
|
@@ -61,7 +62,8 @@ export const SETTINGS_SCHEMA = [
|
|
|
61
62
|
{ key: "TELEGRAM_UI_HOST", label: "Bind Host", category: "miniapp", type: "string", defaultVal: "0.0.0.0", description: "Network interface to bind. Use 127.0.0.1 for local-only access.", restart: true },
|
|
62
63
|
{ key: "TELEGRAM_UI_PUBLIC_HOST", label: "Public Host", category: "miniapp", type: "string", description: "Public hostname if behind a reverse proxy. Auto-detected if not set." },
|
|
63
64
|
{ key: "TELEGRAM_UI_BASE_URL", label: "Base URL Override", category: "miniapp", type: "string", description: "Full public URL (e.g., https://my-domain.com). Takes precedence over auto-detection.", validate: "^https?://" },
|
|
64
|
-
{ key: "TELEGRAM_UI_ALLOW_UNSAFE", label: "Allow Unsafe (No Auth)",
|
|
65
|
+
{ key: "TELEGRAM_UI_ALLOW_UNSAFE", label: "⛔ Allow Unsafe (No Auth)", category: "miniapp", type: "boolean", defaultVal: false, description: "⛔ DANGER: Disables ALL authentication. Anyone with your URL can control agents, read secrets, and execute code on your machine. Combined with a tunnel, this exposes your machine to the ENTIRE INTERNET. Only enable for localhost-only debugging with tunnel DISABLED.", restart: true, danger: true },
|
|
66
|
+
{ key: "_UNSAFE_TUNNEL_WARNING", label: "", category: "miniapp", type: "info", description: "🔴 WARNING: If 'Allow Unsafe' is ON and 'Tunnel Mode' is not disabled, your UI is publicly accessible with ZERO authentication. This is a critical security risk." },
|
|
65
67
|
{ key: "TELEGRAM_UI_AUTH_MAX_AGE_SEC", label: "Auth Token Max Age", category: "miniapp", type: "number", defaultVal: 86400, min: 300, max: 604800, unit: "sec", description: "Maximum age for Telegram initData tokens before they expire." },
|
|
66
68
|
{ key: "TELEGRAM_UI_TUNNEL", label: "Tunnel Mode", category: "miniapp", type: "select", defaultVal: "auto", options: ["auto", "cloudflared", "disabled"], description: "Cloudflare tunnel mode. 'auto' starts tunnel if cloudflared is available.", restart: true },
|
|
67
69
|
|
|
@@ -111,6 +113,14 @@ export const SETTINGS_SCHEMA = [
|
|
|
111
113
|
{ key: "TASK_PLANNER_DEDUP_HOURS", label: "Planner Dedup Window", category: "kanban", type: "number", defaultVal: 6, min: 1, max: 72, unit: "hours", description: "Hours to look back for duplicate task detection.", advanced: true },
|
|
112
114
|
{ key: "BOSUN_PROMPT_PLANNER", label: "Planner Prompt Path", category: "advanced", type: "string", description: "Override the task planner prompt file path.", advanced: true },
|
|
113
115
|
|
|
116
|
+
// ── Logging / Telemetry ──────────────────────────────────────
|
|
117
|
+
{ key: "BOSUN_TELEMETRY_SAMPLE_RATE", label: "Telemetry Sample Rate", category: "logging", type: "number", defaultVal: 1, min: 0, max: 1, description: "Sampling rate for high-volume telemetry events (agent_output/tool events). 1 = full, 0.1 = 10% sample.", advanced: true },
|
|
118
|
+
{ key: "AGENT_WORK_STREAM_RETENTION_DAYS", label: "Stream Retention", category: "logging", type: "number", defaultVal: 30, min: 1, max: 365, unit: "days", description: "How long to keep agent-work stream logs before rotation.", advanced: true },
|
|
119
|
+
{ key: "AGENT_WORK_ERROR_RETENTION_DAYS", label: "Error Retention", category: "logging", type: "number", defaultVal: 90, min: 1, max: 365, unit: "days", description: "How long to keep error logs before rotation.", advanced: true },
|
|
120
|
+
{ key: "AGENT_WORK_SESSION_RETENTION_COUNT", label: "Session Retention Count", category: "logging", type: "number", defaultVal: 100, min: 10, max: 10000, description: "Number of session log files to keep.", advanced: true },
|
|
121
|
+
{ key: "AGENT_WORK_ARCHIVE_RETENTION_DAYS", label: "Archive Retention", category: "logging", type: "number", defaultVal: 180, min: 30, max: 3650, unit: "days", description: "Retention window for compressed log archives.", advanced: true },
|
|
122
|
+
{ key: "AGENT_WORK_METRICS_ROTATION_ENABLED", label: "Rotate Metrics Log", category: "logging", type: "boolean", defaultVal: true, description: "Rotate metrics log monthly to keep file size bounded.", advanced: true },
|
|
123
|
+
|
|
114
124
|
// ── GitHub / Git ─────────────────────────────────────────
|
|
115
125
|
{ key: "GITHUB_TOKEN", label: "GitHub Token", category: "github", type: "secret", sensitive: true, description: "Personal access token or fine-grained token for GitHub API. Required for GitHub kanban backend." },
|
|
116
126
|
{ key: "GITHUB_REPOSITORY", label: "Repository", category: "github", type: "string", description: "GitHub repository in owner/repo format. Auto-detected from git remote if not set.", validate: "^[\\w.-]+/[\\w.-]+$" },
|
package/ui/modules/state.js
CHANGED
|
@@ -43,6 +43,7 @@ const CACHE_TTL = {
|
|
|
43
43
|
threads: 5000, logs: 15000, worktrees: 30000, workspaces: 30000,
|
|
44
44
|
presence: 30000, config: 60000, projects: 60000, git: 20000,
|
|
45
45
|
infra: 30000,
|
|
46
|
+
telemetry: 15000,
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
function _cacheKey(url) { return url; }
|
|
@@ -111,6 +112,12 @@ export const agentLogLines = signal(200);
|
|
|
111
112
|
export const agentLogQuery = signal("");
|
|
112
113
|
export const agentContext = signal(null);
|
|
113
114
|
|
|
115
|
+
// ── Telemetry
|
|
116
|
+
export const telemetrySummary = signal(null);
|
|
117
|
+
export const telemetryErrors = signal([]);
|
|
118
|
+
export const telemetryExecutors = signal({});
|
|
119
|
+
export const telemetryAlerts = signal([]);
|
|
120
|
+
|
|
114
121
|
// ── Config (routing, regions, etc.)
|
|
115
122
|
export const configData = signal(null);
|
|
116
123
|
|
|
@@ -590,6 +597,50 @@ export async function loadConfig() {
|
|
|
590
597
|
_markFresh("config");
|
|
591
598
|
}
|
|
592
599
|
|
|
600
|
+
export async function loadTelemetrySummary() {
|
|
601
|
+
const url = "/api/telemetry/summary";
|
|
602
|
+
if (_cacheFresh(url, "telemetry")) return;
|
|
603
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
604
|
+
ok: false,
|
|
605
|
+
}));
|
|
606
|
+
telemetrySummary.value = res?.data ?? res ?? null;
|
|
607
|
+
_cacheSet(url, telemetrySummary.value);
|
|
608
|
+
_markFresh("telemetry");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function loadTelemetryErrors() {
|
|
612
|
+
const url = "/api/telemetry/errors";
|
|
613
|
+
if (_cacheFresh(url, "telemetry")) return;
|
|
614
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
615
|
+
ok: false,
|
|
616
|
+
}));
|
|
617
|
+
telemetryErrors.value = res?.data ?? res ?? [];
|
|
618
|
+
_cacheSet(url, telemetryErrors.value);
|
|
619
|
+
_markFresh("telemetry");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export async function loadTelemetryExecutors() {
|
|
623
|
+
const url = "/api/telemetry/executors";
|
|
624
|
+
if (_cacheFresh(url, "telemetry")) return;
|
|
625
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
626
|
+
ok: false,
|
|
627
|
+
}));
|
|
628
|
+
telemetryExecutors.value = res?.data ?? res ?? {};
|
|
629
|
+
_cacheSet(url, telemetryExecutors.value);
|
|
630
|
+
_markFresh("telemetry");
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export async function loadTelemetryAlerts() {
|
|
634
|
+
const url = "/api/telemetry/alerts";
|
|
635
|
+
if (_cacheFresh(url, "telemetry")) return;
|
|
636
|
+
const res = await apiFetch(url, { _silent: true }).catch(() => ({
|
|
637
|
+
ok: false,
|
|
638
|
+
}));
|
|
639
|
+
telemetryAlerts.value = res?.data ?? res ?? [];
|
|
640
|
+
_cacheSet(url, telemetryAlerts.value);
|
|
641
|
+
_markFresh("telemetry");
|
|
642
|
+
}
|
|
643
|
+
|
|
593
644
|
/* ═══════════════════════════════════════════════════════════════
|
|
594
645
|
* TAB REFRESH — map tab names to their required loaders
|
|
595
646
|
* ═══════════════════════════════════════════════════════════════ */
|
|
@@ -609,6 +660,13 @@ const TAB_LOADERS = {
|
|
|
609
660
|
control: () => Promise.all([loadExecutor(), loadConfig()]),
|
|
610
661
|
logs: () =>
|
|
611
662
|
Promise.all([loadLogs(), loadAgentLogFileList(), loadAgentLogTailData()]),
|
|
663
|
+
telemetry: () =>
|
|
664
|
+
Promise.all([
|
|
665
|
+
loadTelemetrySummary(),
|
|
666
|
+
loadTelemetryErrors(),
|
|
667
|
+
loadTelemetryExecutors(),
|
|
668
|
+
loadTelemetryAlerts(),
|
|
669
|
+
]),
|
|
612
670
|
settings: () => Promise.all([loadStatus(), loadConfig()]),
|
|
613
671
|
};
|
|
614
672
|
|
|
@@ -687,6 +745,7 @@ const WS_CHANNEL_MAP = {
|
|
|
687
745
|
infra: ["worktrees", "workspaces", "presence"],
|
|
688
746
|
control: ["executor", "overview"],
|
|
689
747
|
logs: ["*"],
|
|
748
|
+
telemetry: ["*"],
|
|
690
749
|
settings: ["overview"],
|
|
691
750
|
};
|
|
692
751
|
|
package/ui/styles/components.css
CHANGED
|
@@ -680,6 +680,24 @@ select.input {
|
|
|
680
680
|
gap: 8px;
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
+
/* ─── Create Task Form ─── */
|
|
684
|
+
.create-task-form {
|
|
685
|
+
gap: 10px;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.create-task-form textarea.input {
|
|
689
|
+
resize: none;
|
|
690
|
+
min-height: 64px;
|
|
691
|
+
line-height: 1.5;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.create-task-advanced-toggle {
|
|
695
|
+
font-size: 12px;
|
|
696
|
+
letter-spacing: 0.02em;
|
|
697
|
+
opacity: 0.75;
|
|
698
|
+
margin-top: -4px;
|
|
699
|
+
}
|
|
700
|
+
|
|
683
701
|
@media (max-width: 640px) {
|
|
684
702
|
.task-card-header {
|
|
685
703
|
flex-direction: column;
|
|
@@ -852,10 +870,16 @@ select.input {
|
|
|
852
870
|
border-bottom: none;
|
|
853
871
|
width: 100%;
|
|
854
872
|
max-width: 500px;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
873
|
+
/* dvh = dynamic viewport height: shrinks when keyboard appears on Android/modern iOS */
|
|
874
|
+
max-height: 92dvh;
|
|
875
|
+
/* Fallback for browsers without dvh — use TG viewport var which updates on keyboard */
|
|
876
|
+
max-height: min(92dvh, calc(var(--viewport-height, 100dvh) - var(--header-height, 60px)));
|
|
877
|
+
overflow: hidden; /* scroll lives in .modal-body */
|
|
878
|
+
display: flex;
|
|
879
|
+
flex-direction: column;
|
|
880
|
+
padding: 0;
|
|
881
|
+
/* Lift above software keyboard on iOS (set by JS via visualViewport API) */
|
|
882
|
+
margin-bottom: var(--keyboard-height, 0px);
|
|
859
883
|
animation: springUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
860
884
|
box-shadow: 0 -10px 30px rgba(1, 4, 9, 0.4);
|
|
861
885
|
}
|
|
@@ -904,9 +928,34 @@ select.input {
|
|
|
904
928
|
flex-direction: column;
|
|
905
929
|
align-items: center;
|
|
906
930
|
background: var(--bg-elevated);
|
|
907
|
-
margin:
|
|
931
|
+
margin: 0;
|
|
908
932
|
padding: 12px 16px;
|
|
909
933
|
border-bottom: 1px solid var(--border);
|
|
934
|
+
flex-shrink: 0;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* ─── Modal scrollable body ─── */
|
|
938
|
+
.modal-body {
|
|
939
|
+
overflow-y: auto;
|
|
940
|
+
overflow-x: hidden;
|
|
941
|
+
overscroll-behavior: contain;
|
|
942
|
+
-webkit-overflow-scrolling: touch;
|
|
943
|
+
padding: 16px;
|
|
944
|
+
padding-bottom: max(16px, calc(var(--safe-bottom, 0px) + 16px));
|
|
945
|
+
flex: 1;
|
|
946
|
+
min-height: 0; /* critical for flex shrink to work */
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/* ─── Modal sticky footer (submit actions) ─── */
|
|
950
|
+
.modal-footer {
|
|
951
|
+
flex-shrink: 0;
|
|
952
|
+
padding: 12px 16px;
|
|
953
|
+
padding-bottom: max(14px, calc(var(--safe-bottom, 0px) + 12px));
|
|
954
|
+
border-top: 1px solid var(--border);
|
|
955
|
+
background: var(--bg-elevated);
|
|
956
|
+
display: flex;
|
|
957
|
+
flex-direction: column;
|
|
958
|
+
gap: 8px;
|
|
910
959
|
}
|
|
911
960
|
|
|
912
961
|
.modal-header .modal-handle {
|
|
@@ -1759,7 +1808,7 @@ select.input {
|
|
|
1759
1808
|
|
|
1760
1809
|
.modal-content {
|
|
1761
1810
|
border-radius: var(--radius-xl);
|
|
1762
|
-
margin-bottom: 24px;
|
|
1811
|
+
margin-bottom: 24px; /* floating above baseline on tablet/desktop */
|
|
1763
1812
|
}
|
|
1764
1813
|
|
|
1765
1814
|
.modal-overlay {
|
|
@@ -2969,7 +3018,7 @@ select.input {
|
|
|
2969
3018
|
|
|
2970
3019
|
/* ─── Modal swipe-to-dismiss ─── */
|
|
2971
3020
|
.modal-content {
|
|
2972
|
-
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1), opacity 0.3s ease;
|
|
3021
|
+
transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1), opacity 0.3s ease, margin-bottom 0.15s ease;
|
|
2973
3022
|
will-change: transform, opacity;
|
|
2974
3023
|
touch-action: pan-y;
|
|
2975
3024
|
overscroll-behavior: contain;
|
|
@@ -2977,7 +3026,10 @@ select.input {
|
|
|
2977
3026
|
|
|
2978
3027
|
.modal-content.modal-dragging {
|
|
2979
3028
|
transition: none;
|
|
2980
|
-
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
.modal-content.modal-dragging .modal-body {
|
|
3032
|
+
overflow-y: hidden; /* freeze inner scroll while drag-dismissing */
|
|
2981
3033
|
}
|
|
2982
3034
|
|
|
2983
3035
|
/* ─── ConfirmDialog ─── */
|
package/ui/styles.css
CHANGED
|
@@ -139,3 +139,91 @@
|
|
|
139
139
|
font-size: 12px;
|
|
140
140
|
text-align: center;
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
/* ─── Telemetry Tab ─── */
|
|
144
|
+
.telemetry-tab {
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
gap: 18px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.telemetry-tab .section-header {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
gap: 12px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.telemetry-summary .metric-grid {
|
|
158
|
+
display: grid;
|
|
159
|
+
gap: 14px;
|
|
160
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.metric-label {
|
|
164
|
+
font-size: 11px;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
letter-spacing: 0.08em;
|
|
167
|
+
color: var(--text-hint);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.metric-value {
|
|
171
|
+
font-size: 20px;
|
|
172
|
+
font-weight: 600;
|
|
173
|
+
color: var(--text-primary);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.telemetry-grid {
|
|
177
|
+
display: grid;
|
|
178
|
+
gap: 16px;
|
|
179
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.telemetry-list {
|
|
183
|
+
list-style: none;
|
|
184
|
+
padding: 0;
|
|
185
|
+
margin: 0;
|
|
186
|
+
display: flex;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
gap: 10px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.telemetry-list li {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: space-between;
|
|
195
|
+
gap: 12px;
|
|
196
|
+
font-size: 13px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.telemetry-label {
|
|
200
|
+
color: var(--text-secondary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.telemetry-count {
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
color: var(--text-primary);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.telemetry-alerts {
|
|
209
|
+
list-style: none;
|
|
210
|
+
padding: 0;
|
|
211
|
+
margin: 0;
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: 12px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.telemetry-alert-title {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 8px;
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
font-size: 13px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.telemetry-alert-meta {
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
color: var(--text-hint);
|
|
228
|
+
margin-top: 4px;
|
|
229
|
+
}
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -1817,6 +1817,7 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
1817
1817
|
const [submitting, setSubmitting] = useState(false);
|
|
1818
1818
|
const [workspaceId, setWorkspaceId] = useState(activeWorkspaceId.value || "");
|
|
1819
1819
|
const [repository, setRepository] = useState("");
|
|
1820
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
1820
1821
|
const activeWsId = activeWorkspaceId.value || "";
|
|
1821
1822
|
|
|
1822
1823
|
const workspaceOptions = managedWorkspaces.value || [];
|
|
@@ -1902,97 +1903,134 @@ function CreateTaskModalInline({ onClose }) {
|
|
|
1902
1903
|
repository,
|
|
1903
1904
|
]);
|
|
1904
1905
|
|
|
1906
|
+
const parsedTags = normalizeTagInput(tagsInput);
|
|
1907
|
+
const hasAdvanced = baseBranch || draft || showAdvanced;
|
|
1908
|
+
|
|
1909
|
+
const footerContent = html`
|
|
1910
|
+
<button
|
|
1911
|
+
class="btn btn-primary"
|
|
1912
|
+
style="width:100%"
|
|
1913
|
+
onClick=${handleSubmit}
|
|
1914
|
+
disabled=${submitting}
|
|
1915
|
+
>
|
|
1916
|
+
${submitting ? "Creating…" : "✓ Create Task"}
|
|
1917
|
+
</button>
|
|
1918
|
+
`;
|
|
1919
|
+
|
|
1905
1920
|
return html`
|
|
1906
|
-
<${Modal}
|
|
1907
|
-
|
|
1921
|
+
<${Modal}
|
|
1922
|
+
title="New Task"
|
|
1923
|
+
onClose=${onClose}
|
|
1924
|
+
contentClassName="modal-content-wide"
|
|
1925
|
+
footer=${footerContent}
|
|
1926
|
+
>
|
|
1927
|
+
<div class="flex-col create-task-form">
|
|
1928
|
+
|
|
1929
|
+
<!-- Title — autofocus so keyboard opens immediately -->
|
|
1908
1930
|
<input
|
|
1909
|
-
class="input
|
|
1910
|
-
placeholder="Task title"
|
|
1931
|
+
class="input"
|
|
1932
|
+
placeholder="Task title *"
|
|
1911
1933
|
value=${title}
|
|
1934
|
+
autoFocus=${true}
|
|
1912
1935
|
onInput=${(e) => setTitle(e.target.value)}
|
|
1936
|
+
onKeyDown=${(e) => e.key === "Enter" && !e.shiftKey && handleSubmit()}
|
|
1913
1937
|
/>
|
|
1938
|
+
|
|
1939
|
+
<!-- Description — compact 2-row textarea -->
|
|
1914
1940
|
<textarea
|
|
1915
|
-
class="input
|
|
1916
|
-
rows="
|
|
1917
|
-
placeholder="
|
|
1941
|
+
class="input"
|
|
1942
|
+
rows="2"
|
|
1943
|
+
placeholder="What needs to be done? (optional)"
|
|
1918
1944
|
value=${description}
|
|
1919
|
-
onInput=${(e) =>
|
|
1945
|
+
onInput=${(e) => {
|
|
1946
|
+
setDescription(e.target.value);
|
|
1947
|
+
// auto-grow up to 6 rows
|
|
1948
|
+
e.target.style.height = "auto";
|
|
1949
|
+
e.target.style.height = Math.min(e.target.scrollHeight, 6 * 24 + 16) + "px";
|
|
1950
|
+
}}
|
|
1920
1951
|
></textarea>
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1952
|
+
|
|
1953
|
+
<!-- Priority — always visible, most commonly changed -->
|
|
1954
|
+
<${SegmentedControl}
|
|
1955
|
+
options=${[
|
|
1956
|
+
{ value: "low", label: "Low" },
|
|
1957
|
+
{ value: "medium", label: "Med" },
|
|
1958
|
+
{ value: "high", label: "High" },
|
|
1959
|
+
{ value: "critical", label: "🔥" },
|
|
1960
|
+
]}
|
|
1961
|
+
value=${priority}
|
|
1962
|
+
onChange=${(v) => { haptic(); setPriority(v); }}
|
|
1926
1963
|
/>
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1964
|
+
|
|
1965
|
+
<!-- Workspace + Repo row -->
|
|
1966
|
+
${workspaceOptions.length > 0 && html`
|
|
1967
|
+
<div class="input-row">
|
|
1968
|
+
<select
|
|
1969
|
+
class="input"
|
|
1970
|
+
value=${workspaceId}
|
|
1971
|
+
onChange=${(e) => setWorkspaceId(e.target.value)}
|
|
1972
|
+
>
|
|
1973
|
+
<option value="">Active workspace</option>
|
|
1974
|
+
${workspaceOptions.map(
|
|
1975
|
+
(ws) => html`<option value=${ws.id}>${ws.name || ws.id}</option>`,
|
|
1976
|
+
)}
|
|
1977
|
+
</select>
|
|
1978
|
+
<select
|
|
1979
|
+
class="input"
|
|
1980
|
+
value=${repository}
|
|
1981
|
+
onChange=${(e) => setRepository(e.target.value)}
|
|
1982
|
+
disabled=${!repositoryOptions.length}
|
|
1983
|
+
>
|
|
1984
|
+
<option value="">
|
|
1985
|
+
${repositoryOptions.length ? "Auto repo" : "No repos"}
|
|
1986
|
+
</option>
|
|
1987
|
+
${repositoryOptions.map(
|
|
1988
|
+
(repo) =>
|
|
1989
|
+
html`<option value=${repo.slug}>${repo.name}${repo.primary ? " ★" : ""}</option>`,
|
|
1990
|
+
)}
|
|
1991
|
+
</select>
|
|
1992
|
+
</div>
|
|
1993
|
+
`}
|
|
1994
|
+
|
|
1995
|
+
<!-- Tags -->
|
|
1953
1996
|
<input
|
|
1954
|
-
class="input
|
|
1955
|
-
placeholder="Tags (comma-separated)"
|
|
1997
|
+
class="input"
|
|
1998
|
+
placeholder="Tags (comma-separated, optional)"
|
|
1956
1999
|
value=${tagsInput}
|
|
1957
2000
|
onInput=${(e) => setTagsInput(e.target.value)}
|
|
1958
2001
|
/>
|
|
1959
|
-
${
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
${normalizeTagInput(tagsInput).map(
|
|
1963
|
-
(tag) => html`<span class="tag-chip">#${tag}</span>`,
|
|
1964
|
-
)}
|
|
2002
|
+
${parsedTags.length > 0 && html`
|
|
2003
|
+
<div class="tag-row">
|
|
2004
|
+
${parsedTags.map((tag) => html`<span class="tag-chip">#${tag}</span>`)}
|
|
1965
2005
|
</div>
|
|
1966
2006
|
`}
|
|
1967
|
-
|
|
2007
|
+
|
|
2008
|
+
<!-- Advanced toggle -->
|
|
2009
|
+
<button
|
|
2010
|
+
class="btn btn-ghost btn-sm create-task-advanced-toggle"
|
|
2011
|
+
style="text-align:left;justify-content:flex-start;gap:6px;padding:6px 0;color:var(--text-hint)"
|
|
2012
|
+
onClick=${() => setShowAdvanced(!showAdvanced)}
|
|
2013
|
+
type="button"
|
|
2014
|
+
>
|
|
2015
|
+
<span style="display:inline-block;transition:transform 0.15s;transform:rotate(${showAdvanced ? 90 : 0}deg)">▶</span>
|
|
2016
|
+
Advanced${hasAdvanced && !showAdvanced ? " •" : ""}
|
|
2017
|
+
</button>
|
|
2018
|
+
|
|
2019
|
+
<!-- Advanced fields: base branch + draft -->
|
|
2020
|
+
${(showAdvanced || hasAdvanced) && html`
|
|
2021
|
+
<input
|
|
2022
|
+
class="input"
|
|
2023
|
+
placeholder="Base branch (optional, e.g. main)"
|
|
2024
|
+
value=${baseBranch}
|
|
2025
|
+
onInput=${(e) => setBaseBranch(e.target.value)}
|
|
2026
|
+
/>
|
|
1968
2027
|
<${Toggle}
|
|
1969
|
-
label="Draft (
|
|
2028
|
+
label="Draft (save to backlog, don't start)"
|
|
1970
2029
|
checked=${draft}
|
|
1971
2030
|
onChange=${(next) => setDraft(next)}
|
|
1972
2031
|
/>
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
<${SegmentedControl}
|
|
1976
|
-
options=${[
|
|
1977
|
-
{ value: "low", label: "Low" },
|
|
1978
|
-
{ value: "medium", label: "Medium" },
|
|
1979
|
-
{ value: "high", label: "High" },
|
|
1980
|
-
{ value: "critical", label: "Critical" },
|
|
1981
|
-
]}
|
|
1982
|
-
value=${priority}
|
|
1983
|
-
onChange=${(v) => {
|
|
1984
|
-
haptic();
|
|
1985
|
-
setPriority(v);
|
|
1986
|
-
}}
|
|
1987
|
-
/>
|
|
1988
|
-
</div>
|
|
1989
|
-
<button
|
|
1990
|
-
class="btn btn-primary modal-form-span"
|
|
1991
|
-
onClick=${handleSubmit}
|
|
1992
|
-
disabled=${submitting}
|
|
1993
|
-
>
|
|
1994
|
-
${submitting ? "Creating…" : "Create Task"}
|
|
1995
|
-
</button>
|
|
2032
|
+
`}
|
|
2033
|
+
|
|
1996
2034
|
</div>
|
|
1997
2035
|
<//>
|
|
1998
2036
|
`;
|