@deck-ui/routines 0.2.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/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@deck-ui/routines",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": ["src"],
8
+ "peerDependencies": {
9
+ "react": "^19.0.0",
10
+ "react-dom": "^19.0.0",
11
+ "@deck-ui/core": "^0.1.0"
12
+ },
13
+ "dependencies": {
14
+ "lucide-react": "^0.577.0",
15
+ "framer-motion": "^12.38.0"
16
+ },
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit"
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Types
2
+ export type {
3
+ Routine,
4
+ RoutineRun,
5
+ RoutineFormState,
6
+ Skill,
7
+ TriggerType,
8
+ RoutineStatus,
9
+ ApprovalMode,
10
+ RunStatus,
11
+ } from "./types"
12
+ export { TRIGGER_LABELS } from "./types"
13
+
14
+ // Components
15
+ export { RoutinesGrid } from "./routines-grid"
16
+ export type { RoutinesGridProps } from "./routines-grid"
17
+
18
+ export { RoutineCard } from "./routine-card"
19
+ export type { RoutineCardProps } from "./routine-card"
20
+
21
+ export { RoutineDetailPage } from "./routine-detail-page"
22
+ export type { RoutineDetailPageProps } from "./routine-detail-page"
23
+
24
+ export { RoutineEditForm } from "./routine-edit-form"
25
+ export type { RoutineEditFormProps } from "./routine-edit-form"
26
+
27
+ export { RoutineDetailActions } from "./routine-detail-actions"
28
+ export type { RoutineDetailActionsProps } from "./routine-detail-actions"
29
+
30
+ export { RoutineRunPage } from "./routine-run-page"
31
+ export type { RoutineRunPageProps } from "./routine-run-page"
32
+
33
+ export { RunHistory } from "./routine-run-history"
34
+ export type { RunHistoryProps } from "./routine-run-history"
@@ -0,0 +1,87 @@
1
+ import type { Routine } from "./types"
2
+ import { TRIGGER_LABELS } from "./types"
3
+ import { cn } from "@deck-ui/core"
4
+ import { Clock } from "lucide-react"
5
+
6
+ export interface RoutineCardProps {
7
+ routine: Routine
8
+ onClick: () => void
9
+ }
10
+
11
+ const STATUS_DOT: Record<string, string> = {
12
+ active: "bg-green-500",
13
+ paused: "bg-[#9b9b9b]",
14
+ needs_setup: "bg-yellow-500",
15
+ error: "bg-red-500",
16
+ }
17
+
18
+ const STATUS_LABEL: Record<string, string> = {
19
+ active: "Active",
20
+ paused: "Paused",
21
+ needs_setup: "Needs Setup",
22
+ error: "Error",
23
+ }
24
+
25
+ export function RoutineCard({ routine, onClick }: RoutineCardProps) {
26
+ const displayName = routine.name || routine.title || "Untitled"
27
+ const triggerLabel =
28
+ TRIGGER_LABELS[routine.trigger_type as keyof typeof TRIGGER_LABELS] ??
29
+ routine.trigger_type
30
+ const lastRun = routine.last_run_at
31
+ ? new Date(routine.last_run_at).toLocaleDateString("en-US", {
32
+ month: "short",
33
+ day: "numeric",
34
+ })
35
+ : null
36
+
37
+ return (
38
+ <button
39
+ onClick={onClick}
40
+ className={cn(
41
+ "w-full text-left rounded-2xl border border-black/[0.08] p-5",
42
+ "bg-white hover:border-black/[0.15] transition-all duration-150",
43
+ "hover:shadow-[0_2px_8px_rgba(0,0,0,0.04)]",
44
+ "flex flex-col gap-3",
45
+ )}
46
+ >
47
+ {/* Top: name + status */}
48
+ <div className="flex items-start gap-2.5">
49
+ <h3 className="text-[15px] font-medium text-[#0d0d0d] leading-snug flex-1 min-w-0">
50
+ {displayName}
51
+ </h3>
52
+ <div className="flex items-center gap-1.5 shrink-0 mt-0.5">
53
+ <div
54
+ className={cn(
55
+ "size-1.5 rounded-full",
56
+ STATUS_DOT[routine.status] ?? "bg-[#9b9b9b]",
57
+ )}
58
+ />
59
+ <span className="text-xs text-[#9b9b9b]">
60
+ {STATUS_LABEL[routine.status] ?? routine.status}
61
+ </span>
62
+ </div>
63
+ </div>
64
+
65
+ {/* Description */}
66
+ {routine.description && (
67
+ <p className="text-sm text-[#5d5d5d] leading-relaxed line-clamp-2">
68
+ {routine.description}
69
+ </p>
70
+ )}
71
+
72
+ {/* Footer: trigger + last run + run count */}
73
+ <div className="flex items-center gap-3 text-xs text-[#9b9b9b] pt-1">
74
+ <span className="flex items-center gap-1">
75
+ <Clock className="size-3" />
76
+ {triggerLabel}
77
+ </span>
78
+ {routine.run_count > 0 && (
79
+ <span>
80
+ {routine.run_count} run{routine.run_count !== 1 ? "s" : ""}
81
+ </span>
82
+ )}
83
+ {lastRun && <span>Last: {lastRun}</span>}
84
+ </div>
85
+ </button>
86
+ )
87
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * RoutineDetailActions — Action buttons for the routine detail page.
3
+ * Save, Run Now, Pause/Resume, Delete.
4
+ */
5
+ import { useState } from "react"
6
+ import { Pause, Play, RefreshCw, Save, Trash2 } from "lucide-react"
7
+ import { cn } from "@deck-ui/core"
8
+
9
+ export interface RoutineDetailActionsProps {
10
+ isActive: boolean
11
+ saving: boolean
12
+ onSave: () => void
13
+ onToggle: () => void
14
+ onRunNow: () => void
15
+ onDelete: () => void
16
+ }
17
+
18
+ export function RoutineDetailActions({
19
+ isActive,
20
+ saving,
21
+ onSave,
22
+ onToggle,
23
+ onRunNow,
24
+ onDelete,
25
+ }: RoutineDetailActionsProps) {
26
+ const [confirmDelete, setConfirmDelete] = useState(false)
27
+
28
+ const handleDelete = () => {
29
+ if (!confirmDelete) {
30
+ setConfirmDelete(true)
31
+ return
32
+ }
33
+ onDelete()
34
+ }
35
+
36
+ const pillBtn = cn(
37
+ "h-9 px-4 text-sm font-medium rounded-full transition-colors",
38
+ "border border-black/[0.1]",
39
+ )
40
+
41
+ return (
42
+ <div className="flex items-center gap-2 pt-4 border-t border-black/[0.06]">
43
+ <button
44
+ onClick={onSave}
45
+ disabled={saving}
46
+ className={cn(
47
+ "h-9 px-4 text-sm font-medium rounded-full",
48
+ "bg-[#0d0d0d] text-white hover:bg-[#0d0d0d]/90 transition-colors",
49
+ "disabled:opacity-50",
50
+ )}
51
+ >
52
+ <span className="flex items-center gap-1.5">
53
+ <Save className="size-3.5" />
54
+ {saving ? "Saving..." : "Save"}
55
+ </span>
56
+ </button>
57
+
58
+ <button onClick={onRunNow} className={cn(pillBtn, "text-[#0d0d0d] hover:bg-[#f5f5f5]")}>
59
+ <span className="flex items-center gap-1.5">
60
+ <RefreshCw className="size-3.5" /> Run Now
61
+ </span>
62
+ </button>
63
+
64
+ <button onClick={onToggle} className={cn(pillBtn, "text-[#0d0d0d] hover:bg-[#f5f5f5]")}>
65
+ <span className="flex items-center gap-1.5">
66
+ {isActive ? (
67
+ <><Pause className="size-3.5" /> Pause</>
68
+ ) : (
69
+ <><Play className="size-3.5" /> Resume</>
70
+ )}
71
+ </span>
72
+ </button>
73
+
74
+ <div className="flex-1" />
75
+
76
+ {confirmDelete ? (
77
+ <div className="flex items-center gap-2">
78
+ <span className="text-xs text-red-600">Delete?</span>
79
+ <button
80
+ onClick={handleDelete}
81
+ className="h-9 px-3 text-sm font-medium rounded-full bg-red-600 text-white hover:bg-red-700 transition-colors"
82
+ >
83
+ Confirm
84
+ </button>
85
+ <button
86
+ onClick={() => setConfirmDelete(false)}
87
+ className={cn(pillBtn, "text-[#0d0d0d] hover:bg-[#f5f5f5]")}
88
+ >
89
+ Cancel
90
+ </button>
91
+ </div>
92
+ ) : (
93
+ <button
94
+ onClick={handleDelete}
95
+ className={cn(pillBtn, "text-[#9b9b9b] hover:text-red-600 hover:border-red-200")}
96
+ >
97
+ <span className="flex items-center gap-1.5">
98
+ <Trash2 className="size-3.5" /> Delete
99
+ </span>
100
+ </button>
101
+ )}
102
+ </div>
103
+ )
104
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * RoutineDetailPage — Full-page detail view for a single routine.
3
+ * Shows editable fields, action buttons, and run history below.
4
+ * All data and actions provided via props (no Zustand, no Tauri).
5
+ */
6
+ import { useCallback, useEffect, useState } from "react"
7
+ import type { Routine, RoutineRun, Skill } from "./types"
8
+ import type { RoutineFormState } from "./routine-edit-form"
9
+ import { RoutineEditForm } from "./routine-edit-form"
10
+ import { RoutineDetailActions } from "./routine-detail-actions"
11
+ import { RunHistory } from "./routine-run-history"
12
+ import { ArrowLeft } from "lucide-react"
13
+
14
+ export interface RoutineDetailPageProps {
15
+ routine: Routine | undefined
16
+ runs: RoutineRun[]
17
+ skills: Skill[]
18
+ onBack: () => void
19
+ onSave: (form: RoutineFormState) => Promise<void>
20
+ onRunNow: () => Promise<void>
21
+ onToggle: () => Promise<void>
22
+ onDelete: () => Promise<void>
23
+ onSelectRun: (runId: string) => void
24
+ }
25
+
26
+ export function RoutineDetailPage({
27
+ routine,
28
+ runs,
29
+ skills,
30
+ onBack,
31
+ onSave,
32
+ onRunNow,
33
+ onToggle,
34
+ onDelete,
35
+ onSelectRun,
36
+ }: RoutineDetailPageProps) {
37
+ const [saving, setSaving] = useState(false)
38
+ const [form, setForm] = useState<RoutineFormState | null>(null)
39
+
40
+ // Initialize form from routine
41
+ useEffect(() => {
42
+ if (routine) {
43
+ setForm({
44
+ name: routine.name || routine.title || "",
45
+ description: routine.description,
46
+ context: routine.context,
47
+ triggerType: routine.trigger_type as RoutineFormState["triggerType"],
48
+ approvalMode: routine.approval_mode as RoutineFormState["approvalMode"],
49
+ skillId: routine.skill_id,
50
+ })
51
+ }
52
+ }, [routine?.id]) // Only reset on routine change, not every re-render
53
+
54
+ const handleChange = useCallback((patch: Partial<RoutineFormState>) => {
55
+ setForm((prev) => (prev ? { ...prev, ...patch } : null))
56
+ }, [])
57
+
58
+ const handleSave = useCallback(async () => {
59
+ if (!form) return
60
+ setSaving(true)
61
+ try {
62
+ await onSave(form)
63
+ } finally {
64
+ setSaving(false)
65
+ }
66
+ }, [form, onSave])
67
+
68
+ if (!routine || !form) {
69
+ return (
70
+ <div className="flex-1 flex items-center justify-center">
71
+ <p className="text-sm text-[#9b9b9b]">Routine not found</p>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ return (
77
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
78
+ {/* Header */}
79
+ <div className="shrink-0 px-6 py-3 border-b border-black/[0.06]">
80
+ <div className="max-w-2xl mx-auto flex items-center gap-3">
81
+ <button
82
+ onClick={onBack}
83
+ className="size-8 flex items-center justify-center rounded-lg text-[#9b9b9b] hover:text-[#0d0d0d] hover:bg-black/[0.05] transition-colors"
84
+ >
85
+ <ArrowLeft className="size-4" />
86
+ </button>
87
+ <h1 className="text-sm font-medium text-[#0d0d0d] truncate flex-1">
88
+ {form.name || "Untitled"}
89
+ </h1>
90
+ </div>
91
+ </div>
92
+
93
+ {/* Scrollable body */}
94
+ <div className="flex-1 overflow-y-auto px-6 py-6">
95
+ <div className="max-w-2xl mx-auto space-y-8">
96
+ <RoutineEditForm form={form} skills={skills} onChange={handleChange} />
97
+
98
+ <RoutineDetailActions
99
+ isActive={routine.status === "active"}
100
+ saving={saving}
101
+ onSave={handleSave}
102
+ onToggle={onToggle}
103
+ onRunNow={onRunNow}
104
+ onDelete={onDelete}
105
+ />
106
+
107
+ <RunHistory runs={runs} onSelectRun={onSelectRun} />
108
+ </div>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * RoutineEditForm — Editable fields for a routine's details.
3
+ * Pure presentational component: data and callbacks via props.
4
+ */
5
+ import type { TriggerType, Skill } from "./types"
6
+ import type { RoutineFormState } from "./types"
7
+ import { cn } from "@deck-ui/core"
8
+
9
+ export type { RoutineFormState }
10
+
11
+ export interface RoutineEditFormProps {
12
+ form: RoutineFormState
13
+ skills: Skill[]
14
+ onChange: (patch: Partial<RoutineFormState>) => void
15
+ }
16
+
17
+ const TRIGGER_OPTIONS: { value: TriggerType; label: string }[] = [
18
+ { value: "manual", label: "Manual" },
19
+ { value: "scheduled", label: "Scheduled" },
20
+ { value: "periodic", label: "Periodic" },
21
+ { value: "on_approval", label: "On Approval" },
22
+ ]
23
+
24
+ const inputClass = cn(
25
+ "w-full px-3 py-2 rounded-xl border border-black/[0.08] bg-white",
26
+ "text-sm text-[#0d0d0d] placeholder:text-[#b4b4b4]",
27
+ "focus:outline-none focus:border-black/20 transition-colors",
28
+ )
29
+
30
+ const labelClass = "text-xs font-medium text-[#8e8e8e] mb-1.5 block"
31
+
32
+ export function RoutineEditForm({
33
+ form,
34
+ skills,
35
+ onChange,
36
+ }: RoutineEditFormProps) {
37
+ return (
38
+ <div className="space-y-5">
39
+ {/* Name */}
40
+ <div>
41
+ <label className={labelClass}>Name</label>
42
+ <input
43
+ type="text"
44
+ value={form.name}
45
+ onChange={(e) => onChange({ name: e.target.value })}
46
+ placeholder="Routine name"
47
+ className={inputClass}
48
+ />
49
+ </div>
50
+
51
+ {/* Description */}
52
+ <div>
53
+ <label className={labelClass}>Description</label>
54
+ <textarea
55
+ value={form.description}
56
+ onChange={(e) => onChange({ description: e.target.value })}
57
+ placeholder="What does this routine do?"
58
+ rows={2}
59
+ className={cn(inputClass, "resize-none")}
60
+ />
61
+ </div>
62
+
63
+ {/* Prompt / Context */}
64
+ <div>
65
+ <label className={labelClass}>Prompt</label>
66
+ <textarea
67
+ value={form.context}
68
+ onChange={(e) => onChange({ context: e.target.value })}
69
+ placeholder="Instructions for the agent when running this routine..."
70
+ rows={4}
71
+ className={cn(inputClass, "resize-none")}
72
+ />
73
+ </div>
74
+
75
+ {/* Two-column: Trigger + Skill */}
76
+ <div className="grid grid-cols-2 gap-4">
77
+ <div>
78
+ <label className={labelClass}>Trigger</label>
79
+ <select
80
+ value={form.triggerType}
81
+ onChange={(e) =>
82
+ onChange({ triggerType: e.target.value as TriggerType })
83
+ }
84
+ className={inputClass}
85
+ >
86
+ {TRIGGER_OPTIONS.map((opt) => (
87
+ <option key={opt.value} value={opt.value}>
88
+ {opt.label}
89
+ </option>
90
+ ))}
91
+ </select>
92
+ </div>
93
+
94
+ <div>
95
+ <label className={labelClass}>Skill</label>
96
+ <select
97
+ value={form.skillId ?? ""}
98
+ onChange={(e) =>
99
+ onChange({ skillId: e.target.value || null })
100
+ }
101
+ className={inputClass}
102
+ >
103
+ <option value="">None</option>
104
+ {skills.map((s) => (
105
+ <option key={s.id} value={s.id}>
106
+ {s.name}
107
+ </option>
108
+ ))}
109
+ </select>
110
+ </div>
111
+ </div>
112
+
113
+ {/* Approval mode */}
114
+ <div>
115
+ <label className={labelClass}>Approval</label>
116
+ <div className="flex gap-2">
117
+ {(["manual", "auto_approve"] as RoutineFormState["approvalMode"][]).map((mode) => (
118
+ <button
119
+ key={mode}
120
+ onClick={() => onChange({ approvalMode: mode })}
121
+ className={cn(
122
+ "h-9 px-4 rounded-full text-sm font-medium transition-colors",
123
+ form.approvalMode === mode
124
+ ? "bg-[#0d0d0d] text-white"
125
+ : "border border-black/[0.1] text-[#5d5d5d] hover:bg-[#f5f5f5]",
126
+ )}
127
+ >
128
+ {mode === "manual" ? "Manual Review" : "Auto-approve"}
129
+ </button>
130
+ ))}
131
+ </div>
132
+ </div>
133
+ </div>
134
+ )
135
+ }
@@ -0,0 +1,87 @@
1
+ import type { RoutineRun } from "./types"
2
+ import {
3
+ Check,
4
+ AlertCircle,
5
+ Clock,
6
+ Loader2,
7
+ } from "lucide-react"
8
+
9
+ export interface RunHistoryProps {
10
+ runs: RoutineRun[]
11
+ onSelectRun: (id: string) => void
12
+ }
13
+
14
+ export function RunHistory({ runs, onSelectRun }: RunHistoryProps) {
15
+ if (runs.length === 0) {
16
+ return (
17
+ <div>
18
+ <p className="text-xs font-medium text-[#9b9b9b] uppercase tracking-wide mb-2">
19
+ Runs
20
+ </p>
21
+ <p className="text-sm text-[#9b9b9b]">No runs yet.</p>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ const sorted = [...runs].sort(
27
+ (a, b) =>
28
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
29
+ )
30
+
31
+ return (
32
+ <div>
33
+ <p className="text-xs font-medium text-[#9b9b9b] uppercase tracking-wide mb-2">
34
+ Run History
35
+ </p>
36
+ <div className="space-y-1">
37
+ {sorted.map((run) => (
38
+ <RunRow key={run.id} run={run} onClick={() => onSelectRun(run.id)} />
39
+ ))}
40
+ </div>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ function RunRow({ run, onClick }: { run: RoutineRun; onClick: () => void }) {
46
+ const date = new Date(run.created_at)
47
+ const dateStr = date.toLocaleDateString("en-US", {
48
+ month: "short",
49
+ day: "numeric",
50
+ })
51
+ const timeStr = date.toLocaleTimeString("en-US", {
52
+ hour: "numeric",
53
+ minute: "2-digit",
54
+ })
55
+
56
+ const icon = (() => {
57
+ switch (run.status) {
58
+ case "running":
59
+ return <Loader2 className="size-3.5 text-blue-500 animate-spin" />
60
+ case "needs_you":
61
+ return <Clock className="size-3.5 text-amber-500" />
62
+ case "done":
63
+ case "approved":
64
+ return <Check className="size-3.5 text-emerald-500" />
65
+ case "error":
66
+ case "failed":
67
+ return <AlertCircle className="size-3.5 text-red-500" />
68
+ default:
69
+ return <Clock className="size-3.5 text-[#9b9b9b]" />
70
+ }
71
+ })()
72
+
73
+ return (
74
+ <button
75
+ onClick={onClick}
76
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-left hover:bg-black/[0.03] transition-colors"
77
+ >
78
+ {icon}
79
+ <span className="text-sm text-[#0d0d0d] truncate flex-1">
80
+ {run.output_title || `${dateStr} ${timeStr}`}
81
+ </span>
82
+ <span className="text-xs text-[#9b9b9b] shrink-0">
83
+ {dateStr} {timeStr}
84
+ </span>
85
+ </button>
86
+ )
87
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * RoutineRunPage — Full-page view for a single routine run.
3
+ * Shows the agent conversation header and delegates chat rendering
4
+ * to a consumer-provided component via the `renderChat` prop.
5
+ */
6
+ import type { Routine, RoutineRun } from "./types"
7
+ import { ArrowLeft, Loader2 } from "lucide-react"
8
+ import { cn } from "@deck-ui/core"
9
+
10
+ export interface RoutineRunPageProps {
11
+ routine: Routine | undefined
12
+ run: RoutineRun | undefined
13
+ onBack: () => void
14
+ /** Render the chat feed area. Receives run status context. */
15
+ renderChat: (context: {
16
+ runId: string
17
+ isRunning: boolean
18
+ isNeedsYou: boolean
19
+ }) => React.ReactNode
20
+ }
21
+
22
+ const RUN_STATUS_LABEL: Record<string, string> = {
23
+ running: "Running",
24
+ needs_you: "Needs You",
25
+ done: "Done",
26
+ approved: "Approved",
27
+ completed: "Completed",
28
+ error: "Error",
29
+ failed: "Failed",
30
+ }
31
+
32
+ export function RoutineRunPage({
33
+ routine,
34
+ run,
35
+ onBack,
36
+ renderChat,
37
+ }: RoutineRunPageProps) {
38
+ const isRunning = run?.status === "running"
39
+ const isNeedsYou = run?.status === "needs_you"
40
+
41
+ const displayName = routine?.name || routine?.title || "Routine"
42
+ const statusLabel = run ? (RUN_STATUS_LABEL[run.status] ?? run.status) : ""
43
+ const runDate = run
44
+ ? new Date(run.created_at).toLocaleDateString("en-US", {
45
+ month: "short",
46
+ day: "numeric",
47
+ hour: "numeric",
48
+ minute: "2-digit",
49
+ })
50
+ : ""
51
+
52
+ return (
53
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
54
+ {/* Header */}
55
+ <div className="shrink-0 px-6 py-3 border-b border-black/[0.06]">
56
+ <div className="max-w-3xl mx-auto flex items-center gap-3">
57
+ <button
58
+ onClick={onBack}
59
+ className="size-7 flex items-center justify-center rounded-md text-[#8e8e8e] hover:text-[#0d0d0d] hover:bg-black/[0.04] transition-colors"
60
+ >
61
+ <ArrowLeft className="size-4" strokeWidth={1.75} />
62
+ </button>
63
+ <div className="min-w-0 flex-1">
64
+ <p className="text-sm font-medium text-[#0d0d0d] truncate">
65
+ {displayName}
66
+ </p>
67
+ <p className="text-[11px] text-[#8e8e8e]">
68
+ {runDate}
69
+ <span className="mx-1">&middot;</span>
70
+ <span className={cn(isRunning && "text-blue-500")}>
71
+ {statusLabel}
72
+ </span>
73
+ </p>
74
+ </div>
75
+ {isRunning && (
76
+ <Loader2 className="size-4 animate-spin text-blue-500 shrink-0" />
77
+ )}
78
+ </div>
79
+ </div>
80
+
81
+ {/* Chat feed — delegated to consumer */}
82
+ {run ? (
83
+ renderChat({
84
+ runId: run.id,
85
+ isRunning,
86
+ isNeedsYou,
87
+ })
88
+ ) : (
89
+ <div className="flex-1 flex items-center justify-center">
90
+ <p className="text-sm text-[#9b9b9b]">Run not found</p>
91
+ </div>
92
+ )}
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * RoutinesGrid — Card grid view for routines.
3
+ * Accepts data and callbacks via props (no Zustand, no Tauri).
4
+ */
5
+ import { useMemo } from "react"
6
+ import type { Routine } from "./types"
7
+ import { RoutineCard } from "./routine-card"
8
+
9
+ export interface RoutinesGridProps {
10
+ routines: Routine[]
11
+ loading?: boolean
12
+ onSelectRoutine: (routineId: string) => void
13
+ }
14
+
15
+ const STATUS_ORDER: Record<string, number> = {
16
+ active: 0,
17
+ needs_setup: 1,
18
+ error: 2,
19
+ paused: 3,
20
+ }
21
+
22
+ export function RoutinesGrid({
23
+ routines,
24
+ loading,
25
+ onSelectRoutine,
26
+ }: RoutinesGridProps) {
27
+ const sorted = useMemo(() => {
28
+ return [...routines].sort((a, b) => {
29
+ const sa = STATUS_ORDER[a.status] ?? 9
30
+ const sb = STATUS_ORDER[b.status] ?? 9
31
+ if (sa !== sb) return sa - sb
32
+ const nameA = (a.name || a.title || "").toLowerCase()
33
+ const nameB = (b.name || b.title || "").toLowerCase()
34
+ return nameA.localeCompare(nameB)
35
+ })
36
+ }, [routines])
37
+
38
+ if (loading && routines.length === 0) {
39
+ return (
40
+ <div className="flex-1 flex items-center justify-center">
41
+ <p className="text-sm text-[#9b9b9b] animate-pulse">
42
+ Loading routines...
43
+ </p>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ if (sorted.length === 0) {
49
+ return (
50
+ <div className="flex-1 flex flex-col items-center pt-[20vh] gap-4 px-8">
51
+ <div className="space-y-2 text-center max-w-md">
52
+ <h1 className="text-2xl font-semibold text-foreground tracking-tight">
53
+ Automate recurring work
54
+ </h1>
55
+ <p className="text-sm text-[#5d5d5d]">
56
+ Routines run on a schedule so Houston can work for you
57
+ automatically — daily reports, weekly research, and more.
58
+ </p>
59
+ </div>
60
+ </div>
61
+ )
62
+ }
63
+
64
+ return (
65
+ <div className="flex-1 overflow-y-auto">
66
+ <div className="max-w-4xl mx-auto px-6 py-6">
67
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
68
+ {sorted.map((routine) => (
69
+ <RoutineCard
70
+ key={routine.id}
71
+ routine={routine}
72
+ onClick={() => onSelectRoutine(routine.id)}
73
+ />
74
+ ))}
75
+ </div>
76
+ </div>
77
+ </div>
78
+ )
79
+ }
package/src/styles.css ADDED
@@ -0,0 +1 @@
1
+ @source ".";
package/src/types.ts ADDED
@@ -0,0 +1,80 @@
1
+ // Generic routine types — mirrors Houston's Routine model without Tauri/backend coupling.
2
+
3
+ export type TriggerType = "on_approval" | "scheduled" | "periodic" | "manual"
4
+ export type RoutineStatus = "active" | "paused" | "needs_setup" | "error"
5
+ export type ApprovalMode = "manual" | "auto_approve"
6
+
7
+ export type RunStatus =
8
+ | "running"
9
+ | "completed"
10
+ | "failed"
11
+ | "approved"
12
+ | "needs_you"
13
+ | "done"
14
+ | "error"
15
+
16
+ export interface Routine {
17
+ id: string
18
+ project_id: string
19
+ goal_id: string | null
20
+ skill_id: string | null
21
+ name: string
22
+ description: string
23
+ trigger_type: TriggerType
24
+ trigger_config: string
25
+ status: RoutineStatus
26
+ approval_mode: ApprovalMode
27
+ context: string
28
+ run_count: number
29
+ approval_count: number
30
+ last_run_at: string | null
31
+ created_at: string
32
+ updated_at: string
33
+ /** v1 compat — may be present from older DBs */
34
+ title?: string
35
+ skill_name?: string | null
36
+ enabled?: boolean
37
+ is_system?: boolean
38
+ }
39
+
40
+ export interface RoutineRun {
41
+ id: string
42
+ routine_id: string
43
+ project_id: string
44
+ status: RunStatus
45
+ session_id: string | null
46
+ claude_session_id: string | null
47
+ output_files: string | null
48
+ cost_usd: number | null
49
+ duration_ms: number | null
50
+ output_title: string | null
51
+ output_summary: string | null
52
+ feedback_text: string | null
53
+ is_test_run: boolean
54
+ created_at: string
55
+ completed_at: string | null
56
+ approved_at: string | null
57
+ }
58
+
59
+ export interface Skill {
60
+ id: string
61
+ name: string
62
+ description: string
63
+ }
64
+
65
+ export const TRIGGER_LABELS: Record<TriggerType, string> = {
66
+ on_approval: "On approval",
67
+ scheduled: "Scheduled",
68
+ periodic: "Periodic",
69
+ manual: "Manual",
70
+ }
71
+
72
+ /** Form state for the routine edit form */
73
+ export interface RoutineFormState {
74
+ name: string
75
+ description: string
76
+ context: string
77
+ triggerType: TriggerType
78
+ approvalMode: ApprovalMode
79
+ skillId: string | null
80
+ }