feed-the-machine 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/bin/generate-manifest.mjs +253 -0
  2. package/bin/install.mjs +372 -26
  3. package/docs/INBOX.md +233 -0
  4. package/ftm/SKILL.md +34 -0
  5. package/ftm-audit/SKILL.md +69 -0
  6. package/ftm-brainstorm/SKILL.md +51 -0
  7. package/ftm-browse/SKILL.md +39 -0
  8. package/ftm-capture/SKILL.md +370 -0
  9. package/ftm-capture.yml +4 -0
  10. package/ftm-codex-gate/SKILL.md +59 -0
  11. package/ftm-config/SKILL.md +35 -0
  12. package/ftm-council/SKILL.md +56 -0
  13. package/ftm-dashboard/SKILL.md +34 -0
  14. package/ftm-debug/SKILL.md +84 -0
  15. package/ftm-diagram/SKILL.md +44 -0
  16. package/ftm-executor/SKILL.md +97 -0
  17. package/ftm-git/SKILL.md +60 -0
  18. package/ftm-inbox/backend/__init__.py +0 -0
  19. package/ftm-inbox/backend/adapters/__init__.py +0 -0
  20. package/ftm-inbox/backend/adapters/_retry.py +64 -0
  21. package/ftm-inbox/backend/adapters/base.py +230 -0
  22. package/ftm-inbox/backend/adapters/freshservice.py +104 -0
  23. package/ftm-inbox/backend/adapters/gmail.py +125 -0
  24. package/ftm-inbox/backend/adapters/jira.py +136 -0
  25. package/ftm-inbox/backend/adapters/registry.py +192 -0
  26. package/ftm-inbox/backend/adapters/slack.py +110 -0
  27. package/ftm-inbox/backend/db/__init__.py +0 -0
  28. package/ftm-inbox/backend/db/connection.py +54 -0
  29. package/ftm-inbox/backend/db/schema.py +78 -0
  30. package/ftm-inbox/backend/executor/__init__.py +7 -0
  31. package/ftm-inbox/backend/executor/engine.py +149 -0
  32. package/ftm-inbox/backend/executor/step_runner.py +98 -0
  33. package/ftm-inbox/backend/main.py +103 -0
  34. package/ftm-inbox/backend/models/__init__.py +1 -0
  35. package/ftm-inbox/backend/models/unified_task.py +36 -0
  36. package/ftm-inbox/backend/planner/__init__.py +6 -0
  37. package/ftm-inbox/backend/planner/generator.py +127 -0
  38. package/ftm-inbox/backend/planner/schema.py +34 -0
  39. package/ftm-inbox/backend/requirements.txt +5 -0
  40. package/ftm-inbox/backend/routes/__init__.py +0 -0
  41. package/ftm-inbox/backend/routes/execute.py +186 -0
  42. package/ftm-inbox/backend/routes/health.py +52 -0
  43. package/ftm-inbox/backend/routes/inbox.py +68 -0
  44. package/ftm-inbox/backend/routes/plan.py +271 -0
  45. package/ftm-inbox/bin/launchagent.mjs +91 -0
  46. package/ftm-inbox/bin/setup.mjs +188 -0
  47. package/ftm-inbox/bin/start.sh +10 -0
  48. package/ftm-inbox/bin/status.sh +17 -0
  49. package/ftm-inbox/bin/stop.sh +8 -0
  50. package/ftm-inbox/config.example.yml +55 -0
  51. package/ftm-inbox/package-lock.json +2898 -0
  52. package/ftm-inbox/package.json +26 -0
  53. package/ftm-inbox/postcss.config.js +6 -0
  54. package/ftm-inbox/src/app.css +199 -0
  55. package/ftm-inbox/src/app.html +18 -0
  56. package/ftm-inbox/src/lib/api.ts +166 -0
  57. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
  58. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
  59. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
  60. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
  61. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
  62. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
  63. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
  64. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
  65. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
  66. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
  67. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
  68. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
  69. package/ftm-inbox/src/lib/theme.ts +47 -0
  70. package/ftm-inbox/src/routes/+layout.svelte +76 -0
  71. package/ftm-inbox/src/routes/+page.svelte +401 -0
  72. package/ftm-inbox/static/favicon.png +0 -0
  73. package/ftm-inbox/svelte.config.js +12 -0
  74. package/ftm-inbox/tailwind.config.ts +63 -0
  75. package/ftm-inbox/tsconfig.json +13 -0
  76. package/ftm-inbox/vite.config.ts +6 -0
  77. package/ftm-intent/SKILL.md +44 -0
  78. package/ftm-manifest.json +3794 -0
  79. package/ftm-map/SKILL.md +50 -0
  80. package/ftm-mind/SKILL.md +173 -66
  81. package/ftm-pause/SKILL.md +43 -0
  82. package/ftm-researcher/SKILL.md +55 -0
  83. package/ftm-resume/SKILL.md +47 -0
  84. package/ftm-retro/SKILL.md +54 -0
  85. package/ftm-routine/SKILL.md +36 -0
  86. package/ftm-state/blackboard/capabilities.json +5 -0
  87. package/ftm-state/blackboard/capabilities.schema.json +27 -0
  88. package/ftm-upgrade/SKILL.md +41 -0
  89. package/hooks/ftm-blackboard-enforcer.sh +28 -27
  90. package/hooks/ftm-plan-gate.sh +21 -25
  91. package/install.sh +238 -111
  92. package/package.json +6 -2
@@ -0,0 +1,271 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import PillButton from '$lib/components/ui/PillButton.svelte';
4
+ import type { PlanStep as PlanStepType } from '$lib/api';
5
+
6
+ export let step: PlanStepType;
7
+ export let index: number;
8
+
9
+ const dispatch = createEventDispatcher<{
10
+ approve: { stepId: number };
11
+ reject: { stepId: number };
12
+ }>();
13
+
14
+ const riskColor: Record<string, string> = {
15
+ low: 'risk-low',
16
+ medium: 'risk-medium',
17
+ high: 'risk-high'
18
+ };
19
+
20
+ const riskEmoji: Record<string, string> = {
21
+ low: '🟢',
22
+ medium: '🟡',
23
+ high: '🔴'
24
+ };
25
+
26
+ const statusEmoji: Record<string, string> = {
27
+ pending: '⏳',
28
+ approved: '✅',
29
+ rejected: '❌',
30
+ running: '⚡',
31
+ completed: '🎉',
32
+ failed: '💥'
33
+ };
34
+
35
+ $: isPending = step.status === 'pending';
36
+ $: isApproved = step.status === 'approved';
37
+ $: isCompleted = step.status === 'completed';
38
+ $: isFailed = step.status === 'failed';
39
+ </script>
40
+
41
+ <div
42
+ class="plan-step"
43
+ class:step-approved={isApproved}
44
+ class:step-completed={isCompleted}
45
+ class:step-failed={isFailed}
46
+ role="listitem"
47
+ >
48
+ <!-- Step number bubble -->
49
+ <div class="step-num" aria-hidden="true">{index}</div>
50
+
51
+ <!-- Main content -->
52
+ <div class="step-content">
53
+ <div class="step-top">
54
+ <span class="step-title">{step.title}</span>
55
+ <span class="step-status" title="Status: {step.status}">
56
+ {statusEmoji[step.status] ?? '⏳'}
57
+ </span>
58
+ </div>
59
+
60
+ <div class="step-meta">
61
+ {#if step.target_system}
62
+ <span class="meta-badge badge-system">{step.target_system}</span>
63
+ {/if}
64
+ <span class="meta-badge {riskColor[step.risk_level] ?? 'risk-low'}">
65
+ {riskEmoji[step.risk_level] ?? '🟢'} {step.risk_level}
66
+ </span>
67
+ {#if step.approval_required}
68
+ <span class="meta-badge badge-approval">approval required</span>
69
+ {/if}
70
+ </div>
71
+
72
+ {#if step.method_primary}
73
+ <div class="step-method">
74
+ <span class="method-label">primary:</span>
75
+ <code class="method-value">{step.method_primary}</code>
76
+ {#if step.method_fallback}
77
+ <span class="method-label">fallback:</span>
78
+ <code class="method-value">{step.method_fallback}</code>
79
+ {/if}
80
+ </div>
81
+ {/if}
82
+
83
+ {#if step.rollback}
84
+ <div class="step-rollback">
85
+ <span class="rollback-label">↩</span>
86
+ <span class="rollback-text">{step.rollback}</span>
87
+ </div>
88
+ {/if}
89
+
90
+ {#if isPending}
91
+ <div class="step-actions">
92
+ <PillButton
93
+ variant="primary"
94
+ size="sm"
95
+ on:click={() => dispatch('approve', { stepId: step.id })}
96
+ >
97
+ Approve
98
+ </PillButton>
99
+ <PillButton
100
+ variant="danger"
101
+ size="sm"
102
+ on:click={() => dispatch('reject', { stepId: step.id })}
103
+ >
104
+ Reject
105
+ </PillButton>
106
+ </div>
107
+ {/if}
108
+ </div>
109
+ </div>
110
+
111
+ <style>
112
+ .plan-step {
113
+ display: flex;
114
+ gap: 0.75rem;
115
+ padding: 0.75rem;
116
+ border-radius: 12px;
117
+ background: var(--bg-secondary, #f8f9fa);
118
+ border: 1.5px solid var(--border-card, #e0e0e0);
119
+ transition:
120
+ border-color 0.2s ease,
121
+ background 0.2s ease,
122
+ transform 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55);
123
+ }
124
+
125
+ .plan-step:hover {
126
+ transform: translateY(-1px);
127
+ }
128
+
129
+ .step-approved {
130
+ border-color: #a5d6a7;
131
+ background: rgba(165, 214, 167, 0.10);
132
+ }
133
+
134
+ .step-completed {
135
+ border-color: #81c784;
136
+ background: rgba(129, 199, 132, 0.12);
137
+ opacity: 0.85;
138
+ }
139
+
140
+ .step-failed {
141
+ border-color: #ef9a9a;
142
+ background: rgba(239, 154, 154, 0.10);
143
+ }
144
+
145
+ /* Step number bubble */
146
+ .step-num {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ width: 26px;
151
+ height: 26px;
152
+ border-radius: 9999px;
153
+ background: var(--border-card, #e0e0e0);
154
+ font-size: 0.7rem;
155
+ font-weight: 800;
156
+ color: var(--text-secondary, #555);
157
+ flex-shrink: 0;
158
+ margin-top: 2px;
159
+ }
160
+
161
+ .step-approved .step-num { background: #c8e6c9; color: #1b5e20; }
162
+ .step-completed .step-num { background: #a5d6a7; color: #1b5e20; }
163
+ .step-failed .step-num { background: #ffcdd2; color: #b71c1c; }
164
+
165
+ /* Content */
166
+ .step-content {
167
+ flex: 1;
168
+ min-width: 0;
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 0.35rem;
172
+ }
173
+
174
+ .step-top {
175
+ display: flex;
176
+ align-items: flex-start;
177
+ justify-content: space-between;
178
+ gap: 0.5rem;
179
+ }
180
+
181
+ .step-title {
182
+ font-size: 0.875rem;
183
+ font-weight: 700;
184
+ color: var(--text-primary, #222);
185
+ line-height: 1.35;
186
+ flex: 1;
187
+ }
188
+
189
+ .step-status {
190
+ font-size: 0.9rem;
191
+ flex-shrink: 0;
192
+ }
193
+
194
+ /* Meta badges */
195
+ .step-meta {
196
+ display: flex;
197
+ flex-wrap: wrap;
198
+ gap: 0.3rem;
199
+ }
200
+
201
+ .meta-badge {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ gap: 0.2rem;
205
+ padding: 2px 8px;
206
+ border-radius: 9999px;
207
+ font-size: 0.65rem;
208
+ font-weight: 700;
209
+ letter-spacing: 0.04em;
210
+ text-transform: uppercase;
211
+ }
212
+
213
+ .badge-system {
214
+ background: rgba(66, 165, 245, 0.15);
215
+ color: #1565c0;
216
+ border: 1px solid rgba(66, 165, 245, 0.3);
217
+ }
218
+
219
+ .risk-low { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
220
+ .risk-medium { background: #fffde7; color: #f57f17; border: 1px solid #ffe082; }
221
+ .risk-high { background: #fce4ec; color: #c62828; border: 1px solid #ef9a9a; }
222
+
223
+ .badge-approval {
224
+ background: rgba(255, 152, 0, 0.12);
225
+ color: #e65100;
226
+ border: 1px solid rgba(255, 152, 0, 0.3);
227
+ }
228
+
229
+ /* Method row */
230
+ .step-method {
231
+ display: flex;
232
+ flex-wrap: wrap;
233
+ align-items: center;
234
+ gap: 0.3rem;
235
+ font-size: 0.72rem;
236
+ }
237
+
238
+ .method-label {
239
+ color: var(--text-muted, #888);
240
+ font-weight: 600;
241
+ }
242
+
243
+ .method-value {
244
+ background: var(--bg-card, #fff);
245
+ border: 1px solid var(--border-card, #e0e0e0);
246
+ border-radius: 4px;
247
+ padding: 1px 5px;
248
+ font-family: 'Menlo', monospace;
249
+ font-size: 0.68rem;
250
+ color: var(--text-secondary, #555);
251
+ }
252
+
253
+ /* Rollback */
254
+ .step-rollback {
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 0.3rem;
258
+ font-size: 0.7rem;
259
+ color: var(--text-muted, #888);
260
+ }
261
+
262
+ .rollback-label { font-size: 0.8rem; }
263
+ .rollback-text { font-style: italic; }
264
+
265
+ /* Actions */
266
+ .step-actions {
267
+ display: flex;
268
+ gap: 0.4rem;
269
+ margin-top: 0.25rem;
270
+ }
271
+ </style>
@@ -0,0 +1,206 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import PlanStep from '$lib/components/PlanStep.svelte';
4
+ import PillButton from '$lib/components/ui/PillButton.svelte';
5
+ import EmptyState from '$lib/components/ui/EmptyState.svelte';
6
+ import type { Plan } from '$lib/api';
7
+ import { approveStep, approveAllSteps } from '$lib/api';
8
+
9
+ export let plan: Plan | null = null;
10
+ export let loading = false;
11
+
12
+ const dispatch = createEventDispatcher<{
13
+ planUpdated: Plan;
14
+ }>();
15
+
16
+ const statusLabel: Record<string, string> = {
17
+ draft: 'Draft',
18
+ approved: 'Approved',
19
+ executing: 'Executing',
20
+ completed: 'Completed',
21
+ failed: 'Failed'
22
+ };
23
+
24
+ const statusColor: Record<string, string> = {
25
+ draft: 'status-draft',
26
+ approved: 'status-approved',
27
+ executing: 'status-executing',
28
+ completed: 'status-completed',
29
+ failed: 'status-failed'
30
+ };
31
+
32
+ $: allLowRisk = plan?.steps.every(s => s.risk_level === 'low') ?? false;
33
+ $: hasPendingSteps = plan?.steps.some(s => s.status === 'pending') ?? false;
34
+ $: approveAllEnabled = hasPendingSteps && allLowRisk && plan?.status === 'draft';
35
+
36
+ async function handleApproveStep(e: CustomEvent<{ stepId: number }>) {
37
+ if (!plan) return;
38
+ try {
39
+ const updated = await approveStep(plan.task_id, e.detail.stepId);
40
+ dispatch('planUpdated', updated);
41
+ } catch (err) {
42
+ console.error('Failed to approve step:', err);
43
+ }
44
+ }
45
+
46
+ async function handleApproveAll() {
47
+ if (!plan) return;
48
+ try {
49
+ const updated = await approveAllSteps(plan.task_id);
50
+ dispatch('planUpdated', updated);
51
+ } catch (err) {
52
+ console.error('Failed to approve all steps:', err);
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <div class="plan-view" aria-label="Execution plan">
58
+ {#if loading}
59
+ <div class="plan-loading">
60
+ <span class="loading-spinner" aria-hidden="true">✨</span>
61
+ <span>Generating plan…</span>
62
+ </div>
63
+
64
+ {:else if !plan || plan.steps.length === 0}
65
+ <EmptyState
66
+ emoji="📋"
67
+ title="No plan generated yet"
68
+ message="Click 'Generate Plan' on a task card to create an execution plan."
69
+ />
70
+
71
+ {:else}
72
+ <!-- Plan header -->
73
+ <div class="plan-header">
74
+ <div class="plan-header-row">
75
+ <span class="steps-count">{plan.steps.length} step{plan.steps.length === 1 ? '' : 's'}</span>
76
+ <span class="plan-status {statusColor[plan.status] ?? 'status-draft'}">
77
+ {statusLabel[plan.status] ?? plan.status}
78
+ </span>
79
+ </div>
80
+
81
+ {#if approveAllEnabled}
82
+ <div class="plan-global-actions">
83
+ <PillButton variant="primary" size="sm" on:click={handleApproveAll}>
84
+ Approve All Low-Risk
85
+ </PillButton>
86
+ </div>
87
+ {/if}
88
+ </div>
89
+
90
+ <!-- Step list -->
91
+ <ol class="steps-list" role="list">
92
+ {#each plan.steps as step, i (step.id)}
93
+ <li class="steps-list-item">
94
+ <PlanStep
95
+ {step}
96
+ index={i + 1}
97
+ on:approve={handleApproveStep}
98
+ on:reject
99
+ />
100
+ </li>
101
+ {/each}
102
+ </ol>
103
+ {/if}
104
+ </div>
105
+
106
+ <style>
107
+ .plan-view {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 0.75rem;
111
+ }
112
+
113
+ /* Loading state */
114
+ .plan-loading {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ gap: 0.6rem;
119
+ padding: 2rem;
120
+ font-size: 0.875rem;
121
+ color: var(--text-muted, #888);
122
+ font-weight: 600;
123
+ }
124
+
125
+ .loading-spinner {
126
+ font-size: 1.2rem;
127
+ animation: spin 1.4s linear infinite;
128
+ }
129
+
130
+ @keyframes spin {
131
+ from { transform: rotate(0deg); }
132
+ to { transform: rotate(360deg); }
133
+ }
134
+
135
+ /* Plan header */
136
+ .plan-header {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 0.5rem;
140
+ }
141
+
142
+ .plan-header-row {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: space-between;
146
+ gap: 0.5rem;
147
+ }
148
+
149
+ .steps-count {
150
+ font-size: 0.72rem;
151
+ font-weight: 800;
152
+ text-transform: uppercase;
153
+ letter-spacing: 0.06em;
154
+ color: var(--text-muted, #888);
155
+ }
156
+
157
+ .plan-status {
158
+ display: inline-flex;
159
+ align-items: center;
160
+ padding: 2px 10px;
161
+ border-radius: 9999px;
162
+ font-size: 0.65rem;
163
+ font-weight: 800;
164
+ text-transform: uppercase;
165
+ letter-spacing: 0.05em;
166
+ }
167
+
168
+ .status-draft { background: #f5f5f5; color: #616161; border: 1px solid #e0e0e0; }
169
+ .status-approved { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
170
+ .status-executing { background: #e3f2fd; color: #1565c0; border: 1px solid #90caf9; }
171
+ .status-completed { background: #f1f8e9; color: #33691e; border: 1px solid #aed581; }
172
+ .status-failed { background: #fce4ec; color: #c62828; border: 1px solid #ef9a9a; }
173
+
174
+ .plan-global-actions {
175
+ display: flex;
176
+ gap: 0.4rem;
177
+ }
178
+
179
+ /* Steps list */
180
+ .steps-list {
181
+ list-style: none;
182
+ padding: 0;
183
+ margin: 0;
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: 0.5rem;
187
+ }
188
+
189
+ .steps-list-item {
190
+ animation: bounceIn 0.35s cubic-bezier(0.68, -0.55, 0.265, 1.55) both;
191
+ }
192
+
193
+ .steps-list-item:nth-child(1) { animation-delay: 0.04s; }
194
+ .steps-list-item:nth-child(2) { animation-delay: 0.08s; }
195
+ .steps-list-item:nth-child(3) { animation-delay: 0.12s; }
196
+ .steps-list-item:nth-child(4) { animation-delay: 0.16s; }
197
+ .steps-list-item:nth-child(5) { animation-delay: 0.20s; }
198
+ .steps-list-item:nth-child(6) { animation-delay: 0.24s; }
199
+ .steps-list-item:nth-child(7) { animation-delay: 0.28s; }
200
+ .steps-list-item:nth-child(8) { animation-delay: 0.32s; }
201
+
202
+ @keyframes bounceIn {
203
+ from { opacity: 0; transform: translateY(10px) scale(0.97); }
204
+ to { opacity: 1; transform: translateY(0) scale(1); }
205
+ }
206
+ </style>
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import { afterUpdate } from 'svelte';
3
+
4
+ export let lines: string[] = [];
5
+ export let progress: string = '';
6
+ export let autoScroll = true;
7
+
8
+ let container: HTMLDivElement;
9
+
10
+ afterUpdate(() => {
11
+ if (autoScroll && container) {
12
+ container.scrollTop = container.scrollHeight;
13
+ }
14
+ });
15
+ </script>
16
+
17
+ <div class="stream-panel">
18
+ {#if progress}
19
+ <div class="progress-bar">{progress}</div>
20
+ {/if}
21
+ <div class="stream-output" bind:this={container}>
22
+ {#each lines as line, i (i)}
23
+ <div class="stream-line">{line}</div>
24
+ {/each}
25
+ {#if lines.length === 0}
26
+ <div class="stream-empty">Waiting for output...</div>
27
+ {/if}
28
+ </div>
29
+ <div class="stream-controls">
30
+ <label class="scroll-toggle">
31
+ <input type="checkbox" bind:checked={autoScroll} />
32
+ <span>Auto-scroll</span>
33
+ </label>
34
+ <span class="line-count">{lines.length} lines</span>
35
+ </div>
36
+ </div>
37
+
38
+ <style>
39
+ .stream-panel {
40
+ display: flex;
41
+ flex-direction: column;
42
+ height: 100%;
43
+ font-family: 'Menlo', 'Courier New', monospace;
44
+ font-size: 0.75rem;
45
+ }
46
+
47
+ .progress-bar {
48
+ padding: 0.4rem 0.75rem;
49
+ background: rgba(76, 175, 80, 0.1);
50
+ border-bottom: 1px solid var(--border-card);
51
+ font-weight: 700;
52
+ color: var(--accent-primary);
53
+ font-size: 0.7rem;
54
+ }
55
+
56
+ .stream-output {
57
+ flex: 1;
58
+ overflow-y: auto;
59
+ padding: 0.5rem 0.75rem;
60
+ }
61
+
62
+ .stream-line {
63
+ padding: 1px 0;
64
+ color: var(--text-secondary);
65
+ white-space: pre-wrap;
66
+ word-break: break-all;
67
+ line-height: 1.5;
68
+ }
69
+
70
+ .stream-empty {
71
+ color: var(--text-muted);
72
+ font-style: italic;
73
+ }
74
+
75
+ .stream-controls {
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: space-between;
79
+ padding: 0.3rem 0.75rem;
80
+ border-top: 1px solid var(--border-card);
81
+ font-size: 0.65rem;
82
+ color: var(--text-muted);
83
+ }
84
+
85
+ .scroll-toggle {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 0.3rem;
89
+ cursor: pointer;
90
+ }
91
+
92
+ .scroll-toggle input {
93
+ margin: 0;
94
+ }
95
+
96
+ .line-count {
97
+ font-variant-numeric: tabular-nums;
98
+ }
99
+ </style>