@houston-ai/routines 0.4.6
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/README.md +42 -0
- package/package.json +22 -0
- package/src/heartbeat-config.tsx +184 -0
- package/src/index.ts +42 -0
- package/src/routine-card.tsx +87 -0
- package/src/routine-detail-actions.tsx +104 -0
- package/src/routine-detail-page.tsx +112 -0
- package/src/routine-edit-form.tsx +158 -0
- package/src/routine-run-history.tsx +87 -0
- package/src/routine-run-page.tsx +95 -0
- package/src/routines-grid.tsx +80 -0
- package/src/schedule-builder.tsx +144 -0
- package/src/schedule-cron-utils.ts +122 -0
- package/src/schedule-picker-fields.tsx +118 -0
- package/src/styles.css +1 -0
- package/src/types.ts +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @deck-ui/routines
|
|
2
|
+
|
|
3
|
+
Automated routine management. Create, edit, run, and review recurring agent tasks with trigger scheduling and approval controls.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @deck-ui/routines
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { RoutinesGrid, RoutineDetailPage } from "@deck-ui/routines"
|
|
15
|
+
|
|
16
|
+
<RoutinesGrid
|
|
17
|
+
routines={routines}
|
|
18
|
+
loading={false}
|
|
19
|
+
onRoutineClick={(r) => navigate(`/routines/${r.id}`)}
|
|
20
|
+
onCreateRoutine={() => openCreateDialog()}
|
|
21
|
+
/>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Exports
|
|
25
|
+
|
|
26
|
+
- `RoutinesGrid` -- card grid of all routines
|
|
27
|
+
- `RoutineCard` -- single routine card with status indicator
|
|
28
|
+
- `RoutineDetailPage` -- full detail with description, triggers, run history
|
|
29
|
+
- `RoutineEditForm` -- create/edit form for routine configuration
|
|
30
|
+
- `RoutineDetailActions` -- action buttons (run, edit, delete)
|
|
31
|
+
- `RoutineRunPage` -- live view of a running routine
|
|
32
|
+
- `RunHistory` -- past run list with status and duration
|
|
33
|
+
- Types: `Routine`, `RoutineRun`, `RoutineFormState`, `TriggerType`, `RoutineStatus`, `ApprovalMode`, `RunStatus`
|
|
34
|
+
|
|
35
|
+
## Peer Dependencies
|
|
36
|
+
|
|
37
|
+
- React 19+
|
|
38
|
+
- @deck-ui/core
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Part of [Keel & Deck](../../README.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@houston-ai/routines",
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"react": "^19.0.0",
|
|
12
|
+
"react-dom": "^19.0.0",
|
|
13
|
+
"@houston-ai/core": "workspace:*"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"lucide-react": "^0.577.0",
|
|
17
|
+
"framer-motion": "^12.38.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeartbeatConfig — Form for configuring agent heartbeat check-ins.
|
|
3
|
+
* Toggle, interval, prompt, active hours, and suppression token.
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from "react"
|
|
6
|
+
import { ChevronDown, ChevronRight } from "lucide-react"
|
|
7
|
+
import { cn } from "@houston-ai/core"
|
|
8
|
+
import type { HeartbeatConfig as HeartbeatConfigType } from "./types"
|
|
9
|
+
|
|
10
|
+
export interface HeartbeatConfigProps {
|
|
11
|
+
config: HeartbeatConfigType
|
|
12
|
+
onChange: (config: HeartbeatConfigType) => void
|
|
13
|
+
loading?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const inputClass = cn(
|
|
17
|
+
"w-full px-3 py-2 rounded-xl border border-border bg-background",
|
|
18
|
+
"text-sm text-foreground placeholder:text-muted-foreground/60",
|
|
19
|
+
"focus:outline-none focus:border-border/80 transition-colors",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const labelClass = "text-xs font-medium text-muted-foreground mb-1.5 block"
|
|
23
|
+
|
|
24
|
+
const INTERVAL_OPTIONS = [
|
|
25
|
+
{ value: 15, label: "Every 15 minutes" },
|
|
26
|
+
{ value: 30, label: "Every 30 minutes" },
|
|
27
|
+
{ value: 60, label: "Every hour" },
|
|
28
|
+
{ value: 120, label: "Every 2 hours" },
|
|
29
|
+
{ value: 240, label: "Every 4 hours" },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export function HeartbeatConfig({
|
|
33
|
+
config,
|
|
34
|
+
onChange,
|
|
35
|
+
loading,
|
|
36
|
+
}: HeartbeatConfigProps) {
|
|
37
|
+
const [showActiveHours, setShowActiveHours] = useState(
|
|
38
|
+
Boolean(config.activeHoursStart || config.activeHoursEnd),
|
|
39
|
+
)
|
|
40
|
+
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
41
|
+
|
|
42
|
+
const update = (patch: Partial<HeartbeatConfigType>) => {
|
|
43
|
+
onChange({ ...config, ...patch })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={cn("space-y-5", loading && "opacity-60 pointer-events-none")}>
|
|
48
|
+
{/* Enable toggle */}
|
|
49
|
+
<div className="flex items-center justify-between">
|
|
50
|
+
<div>
|
|
51
|
+
<p className="text-sm font-medium text-foreground">Heartbeat</p>
|
|
52
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">
|
|
53
|
+
The agent will check in at this interval. If nothing needs
|
|
54
|
+
attention, the check-in is silently suppressed.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => update({ enabled: !config.enabled })}
|
|
59
|
+
className={cn(
|
|
60
|
+
"relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors",
|
|
61
|
+
"border-2 border-transparent cursor-pointer",
|
|
62
|
+
config.enabled ? "bg-primary" : "bg-muted-foreground/20",
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<span
|
|
66
|
+
className={cn(
|
|
67
|
+
"pointer-events-none inline-block size-4 rounded-full bg-white shadow-sm",
|
|
68
|
+
"transition-transform",
|
|
69
|
+
config.enabled ? "translate-x-4" : "translate-x-0",
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{config.enabled && (
|
|
76
|
+
<>
|
|
77
|
+
{/* Interval */}
|
|
78
|
+
<div>
|
|
79
|
+
<label className={labelClass}>Check-in Interval</label>
|
|
80
|
+
<select
|
|
81
|
+
value={config.intervalMinutes}
|
|
82
|
+
onChange={(e) => update({ intervalMinutes: Number(e.target.value) })}
|
|
83
|
+
className={inputClass}
|
|
84
|
+
>
|
|
85
|
+
{INTERVAL_OPTIONS.map((opt) => (
|
|
86
|
+
<option key={opt.value} value={opt.value}>
|
|
87
|
+
{opt.label}
|
|
88
|
+
</option>
|
|
89
|
+
))}
|
|
90
|
+
</select>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Prompt */}
|
|
94
|
+
<div>
|
|
95
|
+
<label className={labelClass}>Heartbeat Prompt</label>
|
|
96
|
+
<textarea
|
|
97
|
+
value={config.prompt}
|
|
98
|
+
onChange={(e) => update({ prompt: e.target.value })}
|
|
99
|
+
placeholder="Check if there's anything that needs attention..."
|
|
100
|
+
rows={3}
|
|
101
|
+
className={cn(inputClass, "resize-none")}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Active hours (collapsible) */}
|
|
106
|
+
<CollapsibleSection
|
|
107
|
+
label="Active Hours"
|
|
108
|
+
open={showActiveHours}
|
|
109
|
+
onToggle={() => setShowActiveHours((v) => !v)}
|
|
110
|
+
>
|
|
111
|
+
<div className="grid grid-cols-2 gap-3">
|
|
112
|
+
<div>
|
|
113
|
+
<label className={labelClass}>Start</label>
|
|
114
|
+
<input
|
|
115
|
+
type="time"
|
|
116
|
+
value={config.activeHoursStart ?? "09:00"}
|
|
117
|
+
onChange={(e) => update({ activeHoursStart: e.target.value })}
|
|
118
|
+
className={inputClass}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<label className={labelClass}>End</label>
|
|
123
|
+
<input
|
|
124
|
+
type="time"
|
|
125
|
+
value={config.activeHoursEnd ?? "22:00"}
|
|
126
|
+
onChange={(e) => update({ activeHoursEnd: e.target.value })}
|
|
127
|
+
className={inputClass}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</CollapsibleSection>
|
|
132
|
+
|
|
133
|
+
{/* Advanced (collapsible) */}
|
|
134
|
+
<CollapsibleSection
|
|
135
|
+
label="Advanced"
|
|
136
|
+
open={showAdvanced}
|
|
137
|
+
onToggle={() => setShowAdvanced((v) => !v)}
|
|
138
|
+
>
|
|
139
|
+
<div>
|
|
140
|
+
<label className={labelClass}>Suppression Token</label>
|
|
141
|
+
<input
|
|
142
|
+
type="text"
|
|
143
|
+
value={config.suppressionToken}
|
|
144
|
+
onChange={(e) => update({ suppressionToken: e.target.value })}
|
|
145
|
+
placeholder="heartbeat_ok"
|
|
146
|
+
className={inputClass}
|
|
147
|
+
/>
|
|
148
|
+
<p className="text-[11px] text-muted-foreground mt-1">
|
|
149
|
+
When the agent responds with this token, the check-in
|
|
150
|
+
is silently suppressed.
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
</CollapsibleSection>
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function CollapsibleSection({
|
|
161
|
+
label,
|
|
162
|
+
open,
|
|
163
|
+
onToggle,
|
|
164
|
+
children,
|
|
165
|
+
}: {
|
|
166
|
+
label: string
|
|
167
|
+
open: boolean
|
|
168
|
+
onToggle: () => void
|
|
169
|
+
children: React.ReactNode
|
|
170
|
+
}) {
|
|
171
|
+
const Arrow = open ? ChevronDown : ChevronRight
|
|
172
|
+
return (
|
|
173
|
+
<div>
|
|
174
|
+
<button
|
|
175
|
+
onClick={onToggle}
|
|
176
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors mb-2"
|
|
177
|
+
>
|
|
178
|
+
<Arrow className="size-3" />
|
|
179
|
+
{label}
|
|
180
|
+
</button>
|
|
181
|
+
{open && <div className="pl-4.5">{children}</div>}
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
Routine,
|
|
4
|
+
RoutineRun,
|
|
5
|
+
RoutineFormState,
|
|
6
|
+
Skill,
|
|
7
|
+
TriggerType,
|
|
8
|
+
RoutineStatus,
|
|
9
|
+
ApprovalMode,
|
|
10
|
+
RunStatus,
|
|
11
|
+
HeartbeatConfig,
|
|
12
|
+
SchedulePreset,
|
|
13
|
+
} from "./types"
|
|
14
|
+
export { TRIGGER_LABELS, SCHEDULE_PRESET_LABELS } from "./types"
|
|
15
|
+
|
|
16
|
+
// Components
|
|
17
|
+
export { RoutinesGrid } from "./routines-grid"
|
|
18
|
+
export type { RoutinesGridProps } from "./routines-grid"
|
|
19
|
+
|
|
20
|
+
export { RoutineCard } from "./routine-card"
|
|
21
|
+
export type { RoutineCardProps } from "./routine-card"
|
|
22
|
+
|
|
23
|
+
export { RoutineDetailPage } from "./routine-detail-page"
|
|
24
|
+
export type { RoutineDetailPageProps } from "./routine-detail-page"
|
|
25
|
+
|
|
26
|
+
export { RoutineEditForm } from "./routine-edit-form"
|
|
27
|
+
export type { RoutineEditFormProps } from "./routine-edit-form"
|
|
28
|
+
|
|
29
|
+
export { RoutineDetailActions } from "./routine-detail-actions"
|
|
30
|
+
export type { RoutineDetailActionsProps } from "./routine-detail-actions"
|
|
31
|
+
|
|
32
|
+
export { RoutineRunPage } from "./routine-run-page"
|
|
33
|
+
export type { RoutineRunPageProps } from "./routine-run-page"
|
|
34
|
+
|
|
35
|
+
export { RunHistory } from "./routine-run-history"
|
|
36
|
+
export type { RunHistoryProps } from "./routine-run-history"
|
|
37
|
+
|
|
38
|
+
export { HeartbeatConfig as HeartbeatConfigPanel } from "./heartbeat-config"
|
|
39
|
+
export type { HeartbeatConfigProps } from "./heartbeat-config"
|
|
40
|
+
|
|
41
|
+
export { ScheduleBuilder } from "./schedule-builder"
|
|
42
|
+
export type { ScheduleBuilderProps } from "./schedule-builder"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Routine } from "./types"
|
|
2
|
+
import { TRIGGER_LABELS } from "./types"
|
|
3
|
+
import { cn } from "@houston-ai/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-success",
|
|
13
|
+
paused: "bg-muted-foreground",
|
|
14
|
+
needs_setup: "bg-yellow-500",
|
|
15
|
+
error: "bg-destructive",
|
|
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 || "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-border p-5",
|
|
42
|
+
"bg-background hover:border-border/80 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-foreground 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-muted-foreground",
|
|
57
|
+
)}
|
|
58
|
+
/>
|
|
59
|
+
<span className="text-xs text-muted-foreground">
|
|
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-muted-foreground 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-muted-foreground 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 "@houston-ai/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-border",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex items-center gap-2 pt-4 border-t border-border">
|
|
43
|
+
<button
|
|
44
|
+
onClick={onSave}
|
|
45
|
+
disabled={saving}
|
|
46
|
+
className={cn(
|
|
47
|
+
"h-9 px-4 text-sm font-medium rounded-full",
|
|
48
|
+
"bg-primary text-primary-foreground hover:bg-primary/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-foreground hover:bg-secondary")}>
|
|
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-foreground hover:bg-secondary")}>
|
|
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-destructive">Delete?</span>
|
|
79
|
+
<button
|
|
80
|
+
onClick={handleDelete}
|
|
81
|
+
className="h-9 px-3 text-sm font-medium rounded-full bg-destructive text-destructive-foreground hover:bg-destructive/90 transition-colors"
|
|
82
|
+
>
|
|
83
|
+
Confirm
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setConfirmDelete(false)}
|
|
87
|
+
className={cn(pillBtn, "text-foreground hover:bg-secondary")}
|
|
88
|
+
>
|
|
89
|
+
Cancel
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
) : (
|
|
93
|
+
<button
|
|
94
|
+
onClick={handleDelete}
|
|
95
|
+
className={cn(pillBtn, "text-muted-foreground hover:text-destructive hover:border-destructive/20")}
|
|
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,
|
|
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-muted-foreground">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-border">
|
|
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-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
|
84
|
+
>
|
|
85
|
+
<ArrowLeft className="size-4" />
|
|
86
|
+
</button>
|
|
87
|
+
<h1 className="text-sm font-medium text-foreground 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
|
+
}
|