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.
@@ -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)", category: "miniapp", type: "boolean", defaultVal: false, description: "⚠️ DANGER: Disables ALL authentication. Only for local development.", restart: true },
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.-]+$" },
@@ -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
 
@@ -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
- max-height: 92vh;
856
- overflow-y: auto;
857
- padding: 20px 16px;
858
- padding-bottom: calc(20px + var(--safe-bottom));
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: -20px -16px 16px;
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
- overflow-y: hidden;
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} title="New Task" onClose=${onClose} contentClassName="modal-content-wide">
1907
- <div class="flex-col gap-md modal-form-grid">
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 modal-form-span"
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 modal-form-span"
1916
- rows="4"
1917
- placeholder="Description"
1941
+ class="input"
1942
+ rows="2"
1943
+ placeholder="What needs to be done? (optional)"
1918
1944
  value=${description}
1919
- onInput=${(e) => setDescription(e.target.value)}
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
- <input
1922
- class="input modal-form-span"
1923
- placeholder="Base branch (optional, e.g. feature/xyz)"
1924
- value=${baseBranch}
1925
- onInput=${(e) => setBaseBranch(e.target.value)}
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
- <div class="input-row modal-form-span">
1928
- <select
1929
- class="input"
1930
- value=${workspaceId}
1931
- onChange=${(e) => setWorkspaceId(e.target.value)}
1932
- >
1933
- <option value="">Active workspace</option>
1934
- ${workspaceOptions.map(
1935
- (ws) => html`<option value=${ws.id}>${ws.name || ws.id}</option>`,
1936
- )}
1937
- </select>
1938
- <select
1939
- class="input"
1940
- value=${repository}
1941
- onChange=${(e) => setRepository(e.target.value)}
1942
- disabled=${!repositoryOptions.length}
1943
- >
1944
- <option value="">
1945
- ${repositoryOptions.length ? "Auto repository" : "No repos in workspace"}
1946
- </option>
1947
- ${repositoryOptions.map(
1948
- (repo) =>
1949
- html`<option value=${repo.slug}>${repo.name}${repo.primary ? " ★" : ""}</option>`,
1950
- )}
1951
- </select>
1952
- </div>
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 modal-form-span"
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
- ${normalizeTagInput(tagsInput).length > 0 &&
1960
- html`
1961
- <div class="tag-row modal-form-span">
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
- <div class="modal-form-span">
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 (keep in backlog)"
2028
+ label="Draft (save to backlog, don't start)"
1970
2029
  checked=${draft}
1971
2030
  onChange=${(next) => setDraft(next)}
1972
2031
  />
1973
- </div>
1974
- <div class="modal-form-span">
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
  `;