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.
- package/bin/generate-manifest.mjs +253 -0
- package/bin/install.mjs +372 -26
- package/docs/INBOX.md +233 -0
- package/ftm/SKILL.md +34 -0
- package/ftm-audit/SKILL.md +69 -0
- package/ftm-brainstorm/SKILL.md +51 -0
- package/ftm-browse/SKILL.md +39 -0
- package/ftm-capture/SKILL.md +370 -0
- package/ftm-capture.yml +4 -0
- package/ftm-codex-gate/SKILL.md +59 -0
- package/ftm-config/SKILL.md +35 -0
- package/ftm-council/SKILL.md +56 -0
- package/ftm-dashboard/SKILL.md +34 -0
- package/ftm-debug/SKILL.md +84 -0
- package/ftm-diagram/SKILL.md +44 -0
- package/ftm-executor/SKILL.md +97 -0
- package/ftm-git/SKILL.md +60 -0
- package/ftm-inbox/backend/__init__.py +0 -0
- package/ftm-inbox/backend/adapters/__init__.py +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -0
- package/ftm-inbox/backend/adapters/base.py +230 -0
- package/ftm-inbox/backend/adapters/freshservice.py +104 -0
- package/ftm-inbox/backend/adapters/gmail.py +125 -0
- package/ftm-inbox/backend/adapters/jira.py +136 -0
- package/ftm-inbox/backend/adapters/registry.py +192 -0
- package/ftm-inbox/backend/adapters/slack.py +110 -0
- package/ftm-inbox/backend/db/__init__.py +0 -0
- package/ftm-inbox/backend/db/connection.py +54 -0
- package/ftm-inbox/backend/db/schema.py +78 -0
- package/ftm-inbox/backend/executor/__init__.py +7 -0
- package/ftm-inbox/backend/executor/engine.py +149 -0
- package/ftm-inbox/backend/executor/step_runner.py +98 -0
- package/ftm-inbox/backend/main.py +103 -0
- package/ftm-inbox/backend/models/__init__.py +1 -0
- package/ftm-inbox/backend/models/unified_task.py +36 -0
- package/ftm-inbox/backend/planner/__init__.py +6 -0
- package/ftm-inbox/backend/planner/generator.py +127 -0
- package/ftm-inbox/backend/planner/schema.py +34 -0
- package/ftm-inbox/backend/requirements.txt +5 -0
- package/ftm-inbox/backend/routes/__init__.py +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -0
- package/ftm-inbox/backend/routes/health.py +52 -0
- package/ftm-inbox/backend/routes/inbox.py +68 -0
- package/ftm-inbox/backend/routes/plan.py +271 -0
- package/ftm-inbox/bin/launchagent.mjs +91 -0
- package/ftm-inbox/bin/setup.mjs +188 -0
- package/ftm-inbox/bin/start.sh +10 -0
- package/ftm-inbox/bin/status.sh +17 -0
- package/ftm-inbox/bin/stop.sh +8 -0
- package/ftm-inbox/config.example.yml +55 -0
- package/ftm-inbox/package-lock.json +2898 -0
- package/ftm-inbox/package.json +26 -0
- package/ftm-inbox/postcss.config.js +6 -0
- package/ftm-inbox/src/app.css +199 -0
- package/ftm-inbox/src/app.html +18 -0
- package/ftm-inbox/src/lib/api.ts +166 -0
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
- package/ftm-inbox/src/lib/theme.ts +47 -0
- package/ftm-inbox/src/routes/+layout.svelte +76 -0
- package/ftm-inbox/src/routes/+page.svelte +401 -0
- package/ftm-inbox/static/favicon.png +0 -0
- package/ftm-inbox/svelte.config.js +12 -0
- package/ftm-inbox/tailwind.config.ts +63 -0
- package/ftm-inbox/tsconfig.json +13 -0
- package/ftm-inbox/vite.config.ts +6 -0
- package/ftm-intent/SKILL.md +44 -0
- package/ftm-manifest.json +3794 -0
- package/ftm-map/SKILL.md +50 -0
- package/ftm-mind/SKILL.md +173 -66
- package/ftm-pause/SKILL.md +43 -0
- package/ftm-researcher/SKILL.md +55 -0
- package/ftm-resume/SKILL.md +47 -0
- package/ftm-retro/SKILL.md +54 -0
- package/ftm-routine/SKILL.md +36 -0
- package/ftm-state/blackboard/capabilities.json +5 -0
- package/ftm-state/blackboard/capabilities.schema.json +27 -0
- package/ftm-upgrade/SKILL.md +41 -0
- package/hooks/ftm-blackboard-enforcer.sh +28 -27
- package/hooks/ftm-plan-gate.sh +21 -25
- package/install.sh +238 -111
- package/package.json +6 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { theme } from '$lib/theme';
|
|
3
|
+
|
|
4
|
+
let rotating = false;
|
|
5
|
+
|
|
6
|
+
function handleToggle() {
|
|
7
|
+
rotating = true;
|
|
8
|
+
theme.toggle();
|
|
9
|
+
setTimeout(() => (rotating = false), 400);
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<button
|
|
14
|
+
class="theme-toggle"
|
|
15
|
+
class:rotating
|
|
16
|
+
on:click={handleToggle}
|
|
17
|
+
aria-label={$theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
|
18
|
+
title={$theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
|
19
|
+
>
|
|
20
|
+
{#if $theme === 'light'}
|
|
21
|
+
<!-- Moon icon -->
|
|
22
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
23
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
24
|
+
</svg>
|
|
25
|
+
{:else}
|
|
26
|
+
<!-- Sun icon -->
|
|
27
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
28
|
+
<circle cx="12" cy="12" r="5" />
|
|
29
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
30
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
31
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
32
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
33
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
34
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
35
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
36
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
37
|
+
</svg>
|
|
38
|
+
{/if}
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
.theme-toggle {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
width: 40px;
|
|
47
|
+
height: 40px;
|
|
48
|
+
border-radius: 9999px;
|
|
49
|
+
border: 2px solid var(--border-card);
|
|
50
|
+
background: var(--bg-card);
|
|
51
|
+
color: var(--text-secondary);
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
transition:
|
|
54
|
+
background 0.2s ease,
|
|
55
|
+
border-color 0.2s ease,
|
|
56
|
+
color 0.2s ease,
|
|
57
|
+
transform 0.18s cubic-bezier(0.68, -0.55, 0.265, 1.55),
|
|
58
|
+
box-shadow 0.18s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.theme-toggle:hover {
|
|
62
|
+
border-color: var(--accent-primary);
|
|
63
|
+
color: var(--accent-primary);
|
|
64
|
+
transform: scale(1.08);
|
|
65
|
+
box-shadow: 0 2px 12px rgba(76, 175, 80, 0.2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.theme-toggle:active {
|
|
69
|
+
transform: scale(0.95);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.theme-toggle.rotating svg {
|
|
73
|
+
animation: spin-once 0.4s ease-in-out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@keyframes spin-once {
|
|
77
|
+
from { transform: rotate(0deg); }
|
|
78
|
+
to { transform: rotate(360deg); }
|
|
79
|
+
}
|
|
80
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
import { browser } from '$app/environment';
|
|
3
|
+
|
|
4
|
+
export type Theme = 'light' | 'dark';
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY = 'ftm-inbox-theme';
|
|
7
|
+
|
|
8
|
+
function getInitialTheme(): Theme {
|
|
9
|
+
if (!browser) return 'light';
|
|
10
|
+
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
|
11
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
12
|
+
// Respect system preference on first visit
|
|
13
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createThemeStore() {
|
|
17
|
+
const { subscribe, set, update } = writable<Theme>(getInitialTheme());
|
|
18
|
+
|
|
19
|
+
function applyTheme(theme: Theme) {
|
|
20
|
+
if (!browser) return;
|
|
21
|
+
document.body.classList.remove('theme-light', 'theme-dark');
|
|
22
|
+
document.body.classList.add(`theme-${theme}`);
|
|
23
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
subscribe,
|
|
28
|
+
set(theme: Theme) {
|
|
29
|
+
applyTheme(theme);
|
|
30
|
+
set(theme);
|
|
31
|
+
},
|
|
32
|
+
toggle() {
|
|
33
|
+
update((current) => {
|
|
34
|
+
const next: Theme = current === 'light' ? 'dark' : 'light';
|
|
35
|
+
applyTheme(next);
|
|
36
|
+
return next;
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
init() {
|
|
40
|
+
const theme = getInitialTheme();
|
|
41
|
+
applyTheme(theme);
|
|
42
|
+
set(theme);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const theme = createThemeStore();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../app.css';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
import { theme } from '$lib/theme';
|
|
5
|
+
import ThemeToggle from '$lib/components/ui/ThemeToggle.svelte';
|
|
6
|
+
|
|
7
|
+
onMount(() => {
|
|
8
|
+
theme.init();
|
|
9
|
+
});
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="app-shell">
|
|
13
|
+
<!-- Top nav bar -->
|
|
14
|
+
<header class="topnav">
|
|
15
|
+
<div class="topnav-brand">
|
|
16
|
+
<span class="brand-dot" aria-hidden="true">◉</span>
|
|
17
|
+
<span class="brand-name">ftm inbox</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="topnav-actions">
|
|
20
|
+
<ThemeToggle />
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<!-- Page content (three-column layout lives in +page.svelte) -->
|
|
25
|
+
<slot />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.app-shell {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
min-height: 100vh;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ─── Top nav ─── */
|
|
36
|
+
.topnav {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: space-between;
|
|
40
|
+
padding: 0.6rem 1.25rem;
|
|
41
|
+
background: var(--bg-card);
|
|
42
|
+
border-bottom: 2px solid var(--border-card);
|
|
43
|
+
position: sticky;
|
|
44
|
+
top: 0;
|
|
45
|
+
z-index: 50;
|
|
46
|
+
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.07);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.topnav-brand {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 0.5rem;
|
|
53
|
+
font-family: 'Nunito', sans-serif;
|
|
54
|
+
font-weight: 800;
|
|
55
|
+
font-size: 1.1rem;
|
|
56
|
+
color: var(--text-primary);
|
|
57
|
+
user-select: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.brand-dot {
|
|
61
|
+
color: var(--accent-primary);
|
|
62
|
+
font-size: 1rem;
|
|
63
|
+
animation: pulse-dot 2.5s ease-in-out infinite;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@keyframes pulse-dot {
|
|
67
|
+
0%, 100% { opacity: 1; }
|
|
68
|
+
50% { opacity: 0.5; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.topnav-actions {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 0.5rem;
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import KawaiiCard from '$lib/components/ui/KawaiiCard.svelte';
|
|
3
|
+
import StatusBadge from '$lib/components/ui/StatusBadge.svelte';
|
|
4
|
+
import PillButton from '$lib/components/ui/PillButton.svelte';
|
|
5
|
+
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
|
6
|
+
import StreamDrawer from '$lib/components/ui/StreamDrawer.svelte';
|
|
7
|
+
import InboxFeed from '$lib/components/InboxFeed.svelte';
|
|
8
|
+
import PlanView from '$lib/components/PlanView.svelte';
|
|
9
|
+
import type { UnifiedTask, Plan } from '$lib/api';
|
|
10
|
+
import { generatePlan, getPlan } from '$lib/api';
|
|
11
|
+
type StatusBadgeStatus = 'pending' | 'planning' | 'approved' | 'executing' | 'complete' | 'failed';
|
|
12
|
+
|
|
13
|
+
// Map arbitrary task status string to a valid StatusBadge value
|
|
14
|
+
function taskStatusBadge(s: string): StatusBadgeStatus {
|
|
15
|
+
const valid: StatusBadgeStatus[] = ['pending', 'planning', 'approved', 'executing', 'complete', 'failed'];
|
|
16
|
+
return (valid.includes(s as StatusBadgeStatus) ? s : 'pending') as StatusBadgeStatus;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const API_BASE = 'http://localhost:8042';
|
|
20
|
+
|
|
21
|
+
const sourceAccent: Record<string, 'blue' | 'green' | 'yellow' | 'coral'> = {
|
|
22
|
+
jira: 'blue',
|
|
23
|
+
freshservice: 'green',
|
|
24
|
+
slack: 'yellow',
|
|
25
|
+
gmail: 'coral'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let selectedTask: UnifiedTask | null = null;
|
|
29
|
+
let currentPlan: Plan | null = null;
|
|
30
|
+
let planLoading = false;
|
|
31
|
+
let drawerOpen = false;
|
|
32
|
+
let drawerLines: string[] = [];
|
|
33
|
+
let auditEntries: { time: string; level: string; msg: string }[] = [];
|
|
34
|
+
|
|
35
|
+
function addAudit(level: 'info' | 'warn' | 'success' | 'error', msg: string) {
|
|
36
|
+
auditEntries = [
|
|
37
|
+
...auditEntries,
|
|
38
|
+
{ time: new Date().toLocaleTimeString('en-GB', { hour12: false }), level, msg }
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function handleSelectTask(e: CustomEvent<UnifiedTask>) {
|
|
43
|
+
selectedTask = e.detail;
|
|
44
|
+
currentPlan = null;
|
|
45
|
+
// Load existing plan for this task if one exists
|
|
46
|
+
try {
|
|
47
|
+
currentPlan = await getPlan(e.detail.id);
|
|
48
|
+
} catch {
|
|
49
|
+
// No plan yet — that's fine
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function handleGeneratePlan(e: CustomEvent<UnifiedTask>) {
|
|
54
|
+
selectedTask = e.detail;
|
|
55
|
+
currentPlan = null;
|
|
56
|
+
planLoading = true;
|
|
57
|
+
drawerLines = [];
|
|
58
|
+
drawerOpen = true;
|
|
59
|
+
|
|
60
|
+
const task = e.detail;
|
|
61
|
+
addAudit('info', `Plan generation started for: ${task.title}`);
|
|
62
|
+
drawerLines = [...drawerLines, `[${new Date().toLocaleTimeString()}] Generate plan: ${task.title}`];
|
|
63
|
+
|
|
64
|
+
// Use SSE stream for live output in the drawer
|
|
65
|
+
try {
|
|
66
|
+
const evtSource = new EventSource(`${API_BASE}/api/tasks/${task.id}/plan-stream`);
|
|
67
|
+
|
|
68
|
+
evtSource.onmessage = (event) => {
|
|
69
|
+
try {
|
|
70
|
+
const msg = JSON.parse(event.data);
|
|
71
|
+
if (msg.type === 'chunk') {
|
|
72
|
+
drawerLines = [...drawerLines, msg.text.trimEnd()];
|
|
73
|
+
} else if (msg.type === 'done') {
|
|
74
|
+
currentPlan = msg.plan as Plan;
|
|
75
|
+
planLoading = false;
|
|
76
|
+
addAudit('success', `Plan ready: ${currentPlan.steps.length} steps`);
|
|
77
|
+
drawerLines = [...drawerLines, `Plan ready — ${currentPlan.steps.length} steps generated.`];
|
|
78
|
+
evtSource.close();
|
|
79
|
+
} else if (msg.type === 'error') {
|
|
80
|
+
addAudit('error', `Plan failed: ${msg.message}`);
|
|
81
|
+
drawerLines = [...drawerLines, `Error: ${msg.message}`];
|
|
82
|
+
planLoading = false;
|
|
83
|
+
evtSource.close();
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Ignore malformed SSE frames
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
evtSource.onerror = () => {
|
|
91
|
+
planLoading = false;
|
|
92
|
+
addAudit('warn', 'SSE stream closed unexpectedly');
|
|
93
|
+
evtSource.close();
|
|
94
|
+
};
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Fallback: direct POST without streaming
|
|
97
|
+
try {
|
|
98
|
+
const plan = await generatePlan(task.id);
|
|
99
|
+
currentPlan = plan;
|
|
100
|
+
addAudit('success', `Plan ready: ${plan.steps.length} steps`);
|
|
101
|
+
} catch (genErr) {
|
|
102
|
+
addAudit('error', `Plan generation failed: ${genErr}`);
|
|
103
|
+
} finally {
|
|
104
|
+
planLoading = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function handlePlanUpdated(e: CustomEvent<Plan>) {
|
|
110
|
+
currentPlan = e.detail;
|
|
111
|
+
addAudit('info', `Plan updated — status: ${e.detail.status}`);
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<!-- Three-column layout + bottom drawer -->
|
|
116
|
+
<div class="layout-grid">
|
|
117
|
+
<!-- Left: Task Inbox -->
|
|
118
|
+
<aside class="sidebar sidebar-left" aria-label="Task inbox">
|
|
119
|
+
<div class="sidebar-header">
|
|
120
|
+
<h2 class="sidebar-title">Inbox</h2>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="sidebar-body">
|
|
123
|
+
<InboxFeed
|
|
124
|
+
selectedTaskId={selectedTask?.id ?? null}
|
|
125
|
+
on:selectTask={handleSelectTask}
|
|
126
|
+
on:generatePlan={handleGeneratePlan}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</aside>
|
|
130
|
+
|
|
131
|
+
<!-- Center: Plan Viewer -->
|
|
132
|
+
<section class="center-panel" aria-label="Plan viewer">
|
|
133
|
+
{#if selectedTask}
|
|
134
|
+
<div class="plan-viewer">
|
|
135
|
+
<div class="plan-header">
|
|
136
|
+
<div class="plan-header-top">
|
|
137
|
+
<span class="plan-id">{selectedTask.source}:{selectedTask.source_id}</span>
|
|
138
|
+
<StatusBadge status={taskStatusBadge(selectedTask.status)} />
|
|
139
|
+
</div>
|
|
140
|
+
<h1 class="plan-title">{selectedTask.title}</h1>
|
|
141
|
+
{#if selectedTask.body}
|
|
142
|
+
<p class="plan-body">{selectedTask.body}</p>
|
|
143
|
+
{/if}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<KawaiiCard accent={sourceAccent[selectedTask.source] ?? 'green'}>
|
|
147
|
+
<span slot="header" class="card-label">Execution Plan</span>
|
|
148
|
+
|
|
149
|
+
<PlanView
|
|
150
|
+
plan={currentPlan}
|
|
151
|
+
loading={planLoading}
|
|
152
|
+
on:planUpdated={handlePlanUpdated}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
<div slot="footer" class="plan-actions">
|
|
156
|
+
<PillButton
|
|
157
|
+
variant="primary"
|
|
158
|
+
size="sm"
|
|
159
|
+
disabled={planLoading}
|
|
160
|
+
on:click={() => { if (selectedTask) handleGeneratePlan(new CustomEvent('generatePlan', { detail: selectedTask })); }}
|
|
161
|
+
>
|
|
162
|
+
{planLoading ? 'Generating…' : currentPlan ? 'Regenerate Plan' : 'Generate Plan'}
|
|
163
|
+
</PillButton>
|
|
164
|
+
{#if selectedTask.source_url}
|
|
165
|
+
<PillButton variant="ghost" size="sm" on:click={() => window.open(selectedTask?.source_url ?? '', '_blank')}>
|
|
166
|
+
Open Source
|
|
167
|
+
</PillButton>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
</KawaiiCard>
|
|
171
|
+
</div>
|
|
172
|
+
{:else}
|
|
173
|
+
<EmptyState
|
|
174
|
+
emoji="🗂️"
|
|
175
|
+
title="Select a task"
|
|
176
|
+
message="Choose a task from the inbox to view its plan."
|
|
177
|
+
/>
|
|
178
|
+
{/if}
|
|
179
|
+
</section>
|
|
180
|
+
|
|
181
|
+
<!-- Right: Audit Log -->
|
|
182
|
+
<aside class="sidebar sidebar-right" aria-label="Audit log">
|
|
183
|
+
<div class="sidebar-header">
|
|
184
|
+
<h2 class="sidebar-title">Audit Log</h2>
|
|
185
|
+
{#if auditEntries.length > 0}
|
|
186
|
+
<span class="sidebar-count">{auditEntries.length}</span>
|
|
187
|
+
{/if}
|
|
188
|
+
</div>
|
|
189
|
+
<div class="sidebar-body">
|
|
190
|
+
{#if auditEntries.length === 0}
|
|
191
|
+
<EmptyState
|
|
192
|
+
emoji="📋"
|
|
193
|
+
title="No events yet"
|
|
194
|
+
message="Audit events will appear here."
|
|
195
|
+
/>
|
|
196
|
+
{:else}
|
|
197
|
+
<div class="audit-list">
|
|
198
|
+
{#each auditEntries as entry, i (i)}
|
|
199
|
+
<div class="audit-entry audit-{entry.level}">
|
|
200
|
+
<span class="audit-time">{entry.time}</span>
|
|
201
|
+
<span class="audit-msg">{entry.msg}</span>
|
|
202
|
+
</div>
|
|
203
|
+
{/each}
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
</aside>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- Bottom drawer for streaming agent output -->
|
|
211
|
+
<StreamDrawer bind:open={drawerOpen} lines={drawerLines} />
|
|
212
|
+
|
|
213
|
+
<style>
|
|
214
|
+
/* ─── Three-column layout ─── */
|
|
215
|
+
.layout-grid {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex: 1;
|
|
218
|
+
height: calc(100vh - 57px - 36px); /* minus nav height minus drawer handle */
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.sidebar {
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: column;
|
|
225
|
+
background: var(--bg-sidebar);
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
flex-shrink: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sidebar-left {
|
|
231
|
+
width: 280px;
|
|
232
|
+
border-right: 1px solid var(--border-card);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.sidebar-right {
|
|
236
|
+
width: 320px;
|
|
237
|
+
border-left: 1px solid var(--border-card);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.sidebar-header {
|
|
241
|
+
display: flex;
|
|
242
|
+
align-items: center;
|
|
243
|
+
justify-content: space-between;
|
|
244
|
+
padding: 0.75rem 1rem 0.5rem;
|
|
245
|
+
border-bottom: 1px solid var(--border-card);
|
|
246
|
+
background: var(--bg-card);
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.sidebar-title {
|
|
251
|
+
font-size: 0.72rem;
|
|
252
|
+
font-weight: 800;
|
|
253
|
+
letter-spacing: 0.08em;
|
|
254
|
+
text-transform: uppercase;
|
|
255
|
+
color: var(--text-muted);
|
|
256
|
+
margin: 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.sidebar-count {
|
|
260
|
+
font-size: 0.68rem;
|
|
261
|
+
font-weight: 800;
|
|
262
|
+
background: var(--border-card);
|
|
263
|
+
color: var(--text-muted);
|
|
264
|
+
padding: 2px 7px;
|
|
265
|
+
border-radius: 9999px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.sidebar-body {
|
|
269
|
+
flex: 1;
|
|
270
|
+
overflow-y: auto;
|
|
271
|
+
padding: 0.75rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.center-panel {
|
|
275
|
+
flex: 1;
|
|
276
|
+
overflow-y: auto;
|
|
277
|
+
padding: 1.25rem;
|
|
278
|
+
min-width: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ─── Plan viewer ─── */
|
|
282
|
+
.plan-viewer {
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-direction: column;
|
|
285
|
+
gap: 1rem;
|
|
286
|
+
max-width: 680px;
|
|
287
|
+
margin: 0 auto;
|
|
288
|
+
animation: fadeUp 0.3s ease-out both;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@keyframes fadeUp {
|
|
292
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
293
|
+
to { opacity: 1; transform: translateY(0); }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.plan-header {
|
|
297
|
+
display: flex;
|
|
298
|
+
flex-direction: column;
|
|
299
|
+
gap: 0.4rem;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.plan-header-top {
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
gap: 0.75rem;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.plan-id {
|
|
309
|
+
font-size: 0.75rem;
|
|
310
|
+
font-weight: 800;
|
|
311
|
+
letter-spacing: 0.06em;
|
|
312
|
+
color: var(--text-muted);
|
|
313
|
+
font-family: 'Menlo', monospace;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.plan-title {
|
|
317
|
+
font-size: 1.25rem;
|
|
318
|
+
font-weight: 800;
|
|
319
|
+
color: var(--text-primary);
|
|
320
|
+
line-height: 1.3;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.plan-body {
|
|
324
|
+
font-size: 0.85rem;
|
|
325
|
+
color: var(--text-secondary);
|
|
326
|
+
line-height: 1.5;
|
|
327
|
+
margin: 0;
|
|
328
|
+
max-height: 100px;
|
|
329
|
+
overflow-y: auto;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.card-label {
|
|
333
|
+
font-size: 0.72rem;
|
|
334
|
+
font-weight: 800;
|
|
335
|
+
text-transform: uppercase;
|
|
336
|
+
letter-spacing: 0.08em;
|
|
337
|
+
color: var(--text-muted);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
.plan-actions {
|
|
342
|
+
display: flex;
|
|
343
|
+
gap: 0.5rem;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ─── Audit log ─── */
|
|
348
|
+
.audit-list {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
gap: 0.25rem;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.audit-entry {
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-direction: column;
|
|
357
|
+
gap: 0.15rem;
|
|
358
|
+
padding: 0.4rem 0.5rem;
|
|
359
|
+
border-radius: 8px;
|
|
360
|
+
font-size: 0.72rem;
|
|
361
|
+
border-left: 3px solid transparent;
|
|
362
|
+
transition: background 0.1s;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.audit-entry:hover { background: rgba(76, 175, 80, 0.04); }
|
|
366
|
+
|
|
367
|
+
.audit-info { border-left-color: #66bb6a; }
|
|
368
|
+
.audit-warn { border-left-color: #ffd54f; background: rgba(255, 213, 79, 0.06); }
|
|
369
|
+
.audit-success { border-left-color: #4caf50; background: rgba(76, 175, 80, 0.06); }
|
|
370
|
+
.audit-error { border-left-color: #ef5350; background: rgba(239, 83, 80, 0.06); }
|
|
371
|
+
|
|
372
|
+
.audit-time {
|
|
373
|
+
font-family: 'Menlo', monospace;
|
|
374
|
+
font-size: 0.65rem;
|
|
375
|
+
color: var(--text-muted);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.audit-msg {
|
|
379
|
+
color: var(--text-secondary);
|
|
380
|
+
font-weight: 600;
|
|
381
|
+
line-height: 1.4;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* ─── Mobile responsive ─── */
|
|
385
|
+
@media (max-width: 768px) {
|
|
386
|
+
.layout-grid {
|
|
387
|
+
flex-direction: column;
|
|
388
|
+
height: auto;
|
|
389
|
+
overflow: visible;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.sidebar-left,
|
|
393
|
+
.sidebar-right {
|
|
394
|
+
width: 100%;
|
|
395
|
+
border-right: none;
|
|
396
|
+
border-left: none;
|
|
397
|
+
border-bottom: 1px solid var(--border-card);
|
|
398
|
+
max-height: 40vh;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
</style>
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import adapter from '@sveltejs/adapter-auto';
|
|
2
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
3
|
+
|
|
4
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
5
|
+
const config = {
|
|
6
|
+
preprocess: vitePreprocess(),
|
|
7
|
+
kit: {
|
|
8
|
+
adapter: adapter()
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default config;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
content: ['./src/**/*.{html,js,svelte,ts}'],
|
|
5
|
+
darkMode: 'class',
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
colors: {
|
|
9
|
+
kawaii: {
|
|
10
|
+
mint: {
|
|
11
|
+
50: '#e8f5e9',
|
|
12
|
+
100: '#c8e6c9',
|
|
13
|
+
200: '#a5d6a7',
|
|
14
|
+
300: '#81c784',
|
|
15
|
+
400: '#66bb6a',
|
|
16
|
+
500: '#4caf50',
|
|
17
|
+
600: '#43a047',
|
|
18
|
+
700: '#388e3c',
|
|
19
|
+
800: '#2e7d32',
|
|
20
|
+
900: '#1b5e20',
|
|
21
|
+
950: '#1b2e1b'
|
|
22
|
+
},
|
|
23
|
+
cream: '#fefefe',
|
|
24
|
+
coral: '#ffccbc',
|
|
25
|
+
yellow: '#fff9c4',
|
|
26
|
+
blue: '#bbdefb',
|
|
27
|
+
orange: '#ffe0b2',
|
|
28
|
+
teal: '#b2dfdb',
|
|
29
|
+
red: '#ffcdd2',
|
|
30
|
+
neon: '#69f0ae'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
borderRadius: {
|
|
34
|
+
kawaii: '16px',
|
|
35
|
+
pill: '9999px'
|
|
36
|
+
},
|
|
37
|
+
fontFamily: {
|
|
38
|
+
kawaii: ['Nunito', 'Quicksand', 'sans-serif']
|
|
39
|
+
},
|
|
40
|
+
boxShadow: {
|
|
41
|
+
kawaii: '0 4px 24px 0 rgba(76, 175, 80, 0.10)',
|
|
42
|
+
'kawaii-md': '0 6px 32px 0 rgba(76, 175, 80, 0.15)',
|
|
43
|
+
'kawaii-glow': '0 0 0 2px #69f0ae, 0 4px 24px 0 rgba(105, 240, 174, 0.25)'
|
|
44
|
+
},
|
|
45
|
+
animation: {
|
|
46
|
+
'bounce-in': 'bounceIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) both',
|
|
47
|
+
'fade-up': 'fadeUp 0.3s ease-out both',
|
|
48
|
+
'spin-once': 'spin 0.4s ease-in-out'
|
|
49
|
+
},
|
|
50
|
+
keyframes: {
|
|
51
|
+
bounceIn: {
|
|
52
|
+
'0%': { opacity: '0', transform: 'scale(0.8)' },
|
|
53
|
+
'100%': { opacity: '1', transform: 'scale(1)' }
|
|
54
|
+
},
|
|
55
|
+
fadeUp: {
|
|
56
|
+
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
|
57
|
+
'100%': { opacity: '1', transform: 'translateY(0)' }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
plugins: []
|
|
63
|
+
} satisfies Config;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"checkJs": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true
|
|
12
|
+
}
|
|
13
|
+
}
|