Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeb2ee4b82 | ||
|
|
c3033fe304 | ||
|
|
5dcd76a9a9 | ||
|
|
8d98efc900 | ||
|
|
889058db1b | ||
|
|
b3ed7db75c | ||
|
|
add028eed0 | ||
|
|
12b2f33572 | ||
|
|
f90ddc345f | ||
|
|
63195d1ec8 | ||
|
|
ae8e43c298 | ||
|
|
94ff623708 | ||
|
|
387a336520 | ||
|
|
048e835b31 | ||
|
|
f46ab50ff9 | ||
|
|
0a43a1f7d2 | ||
|
|
155af91e66 | ||
|
|
eb32422e05 | ||
|
|
d2e65d8821 | ||
|
|
1bb5d8d534 | ||
|
|
8ae5e7a9e8 | ||
|
|
ca46bf4518 | ||
|
|
3ea49421bb | ||
|
|
d162ba205e | ||
|
|
01a99edd52 | ||
|
|
2216092ef4 | ||
|
|
e966836886 | ||
|
|
ecf8ee56af | ||
|
|
2432207e5b | ||
|
|
a6eca2561b | ||
|
|
6b7290dc42 | ||
|
|
fb85dfa6f9 | ||
|
|
4642a2f25b | ||
|
|
6008b4914c | ||
|
|
6caa82597e | ||
|
|
3196c4957f | ||
|
|
1fb4786f18 | ||
|
|
2d26af5934 | ||
|
|
26cc1b3154 | ||
|
|
0f2f5180ee | ||
|
|
257a333904 | ||
|
|
94abebf904 | ||
|
|
f3e164b834 | ||
|
|
3b352fe654 | ||
|
|
9331055b44 | ||
|
|
d47d01495c | ||
|
|
c233b7607d | ||
|
|
c744c8ce4b | ||
|
|
1d75e8e1cd | ||
|
|
eb32396bd4 | ||
|
|
f204e91f29 | ||
|
|
1151a0ce80 | ||
|
|
a67a768592 | ||
|
|
699bd40423 | ||
|
|
bfa128f131 | ||
|
|
f0bb2ffcce | ||
|
|
301322a6b0 | ||
|
|
53a61d9269 | ||
|
|
ad807fb0df | ||
|
|
5ee759d0fd | ||
|
|
eda5d04710 | ||
|
|
329aa8a360 | ||
|
|
9945290bdd | ||
|
|
4242cdc490 | ||
|
|
6601056972 | ||
|
|
1c424f26e7 | ||
|
|
6e6546fe32 | ||
|
|
e329f1cc6f | ||
|
|
29ac9b3caf | ||
|
|
71b21e1974 | ||
|
|
96c6cfc6c5 | ||
|
|
aa742eed79 | ||
|
|
1caaf5f4dd | ||
|
|
bdc46a4fdd | ||
|
|
4043c2cbe0 | ||
|
|
b0a826cfd4 | ||
|
|
18d4c5978b | ||
|
|
2d64740007 | ||
|
|
5f162a6668 | ||
|
|
5fb3df84ed | ||
|
|
41fd586ca0 | ||
|
|
1787147e82 | ||
|
|
0d333e8470 | ||
|
|
816e60d028 | ||
|
|
b99548a900 | ||
|
|
4ff004790d | ||
|
|
c378503e26 | ||
|
|
8a5c626892 | ||
|
|
85e275a362 | ||
|
|
9c1597a450 | ||
|
|
d4369aabc3 | ||
|
|
7b30748bf5 | ||
|
|
a5221792ba | ||
|
|
79342277b8 |
@@ -29,7 +29,8 @@
|
||||
"Bash(flutter *)",
|
||||
"Bash(make *)",
|
||||
"Bash(pql)",
|
||||
"Bash(pql *)"
|
||||
"Bash(pql *)",
|
||||
"Bash(awk *)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf /*)",
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: d2-diagram
|
||||
description: >
|
||||
Generate technical diagrams using d2 (text-to-diagram CLI). Use when the
|
||||
user says "create a diagram", "draw architecture", "make a flowchart",
|
||||
"diagram this", "render d2", "d2", "data flow diagram", "entity relationship",
|
||||
"state machine", "sequence diagram", "UI flow", or invokes /d2-diagram.
|
||||
Produces .d2 source files and renders them to PNG. Also use when asked
|
||||
to update, re-render, or batch render existing diagrams.
|
||||
---
|
||||
|
||||
# d2 Diagram Generation
|
||||
|
||||
Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no
|
||||
external dependencies beyond the d2 binary.
|
||||
|
||||
**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2`
|
||||
|
||||
## Project Defaults
|
||||
|
||||
| Setting | Value | Override |
|
||||
|---------|-------|----------|
|
||||
| Theme | 200 (Dark Mauve) | `--theme N` |
|
||||
| Layout | dagre | `--layout elk` |
|
||||
| Padding | 100px | — |
|
||||
| Format | PNG | `--svg` |
|
||||
|
||||
## Output Convention
|
||||
|
||||
```
|
||||
docs/diagrams/
|
||||
architecture/ # System architecture, IPC, component layout
|
||||
data-flow/ # Sequence diagrams, data pipelines
|
||||
entity/ # ER diagrams, ECS component schemas
|
||||
state/ # State machines, behavior trees
|
||||
ui/ # UI navigation flow, screen transitions
|
||||
```
|
||||
|
||||
Both `.d2` source and `.png` output are tracked in git.
|
||||
|
||||
## Single Diagram Workflow
|
||||
|
||||
1. **Determine category** — architecture, data-flow, entity, state, or ui
|
||||
2. **Read template** — `references/diagram-templates.md` for the matching category
|
||||
3. **Read syntax** — `references/d2-syntax-guide.md` if unfamiliar with d2 syntax
|
||||
4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2`
|
||||
5. **Validate** — `.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}`
|
||||
6. **Render** — `.claude/skills/d2-diagram/scripts/d2-render.sh {file}`
|
||||
7. **Read SVG** — verify the output, present to user
|
||||
|
||||
### Script Usage
|
||||
|
||||
```bash
|
||||
# Render with project defaults
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2
|
||||
|
||||
# Validate syntax only
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2
|
||||
|
||||
# Auto-format source
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2
|
||||
|
||||
# Sketch mode (hand-drawn look for drafts)
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch
|
||||
|
||||
# Light theme (for printable docs)
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0
|
||||
|
||||
# SVG output (if specifically needed)
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg
|
||||
```
|
||||
|
||||
## Batch Render
|
||||
|
||||
Re-render all diagrams after theme or style changes:
|
||||
|
||||
```bash
|
||||
# All diagrams
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh
|
||||
|
||||
# One category
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/
|
||||
|
||||
# Preview what would render
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run
|
||||
|
||||
# Force re-render everything
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh --force
|
||||
```
|
||||
|
||||
Batch skips files whose PNG is newer than the `.d2` source unless `--force`.
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Variables for consistent styling
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-bg: "#2a3040"
|
||||
color-stroke: "#333340"
|
||||
color-text: "#c8d0e0"
|
||||
color-accent: "#c8d8f0"
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-board (layers)
|
||||
|
||||
```d2
|
||||
# Base diagram here
|
||||
|
||||
layers: {
|
||||
detailed: {
|
||||
# More detailed view
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sequence diagrams
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
client: Godot Client
|
||||
server: Rust Server
|
||||
|
||||
client -> server: TickRequest(delta)
|
||||
server -> client: WorldState(entities)
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
Split shared definitions into a separate file and import:
|
||||
|
||||
```d2
|
||||
...@shared-defs.d2
|
||||
```
|
||||
|
||||
## Agent Guidance
|
||||
|
||||
- **Qatux** — Architecture decision records, system overview diagrams, data
|
||||
schemas. Prefer architecture and entity templates.
|
||||
- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception
|
||||
system data flow. Prefer architecture and data-flow templates.
|
||||
- **Araminta** — UI navigation flow, screen transitions, component hierarchy.
|
||||
Prefer UI flow template.
|
||||
|
||||
## References
|
||||
|
||||
- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges,
|
||||
containers, styling, variables). Read when unfamiliar with d2 syntax.
|
||||
- `references/diagram-templates.md` — Five category templates with complete
|
||||
d2 source examples. Read when starting a new diagram.
|
||||
@@ -0,0 +1,212 @@
|
||||
# D2 Language Quick Reference
|
||||
|
||||
## Nodes
|
||||
|
||||
```d2
|
||||
server # Implicit label from key
|
||||
server: Simulation Server # Explicit label
|
||||
server: Simulation Server { # With properties
|
||||
shape: hexagon
|
||||
style.fill: "#2d3436"
|
||||
}
|
||||
```
|
||||
|
||||
## Edges
|
||||
|
||||
```d2
|
||||
a -> b # Directed
|
||||
a <- b # Reverse directed
|
||||
a <-> b # Bidirectional
|
||||
a -- b # Undirected
|
||||
a -> b: "label" # Labeled edge
|
||||
a -> b -> c # Chained
|
||||
```
|
||||
|
||||
## Containers (nesting)
|
||||
|
||||
```d2
|
||||
infrastructure: {
|
||||
server: Simulation Server
|
||||
database: State Store {
|
||||
shape: cylinder
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Shapes
|
||||
|
||||
| Shape | Use for |
|
||||
|-------|---------|
|
||||
| `rectangle` | Default. Components, modules, generic. |
|
||||
| `hexagon` | Systems, services, major components. |
|
||||
| `cylinder` | Databases, storage, persistent state. |
|
||||
| `diamond` | Decisions, conditions, branch points. |
|
||||
| `oval` / `circle` | Start/end states, events. |
|
||||
| `cloud` | External systems, networks. |
|
||||
| `person` | Actors, users, NPCs. |
|
||||
| `queue` | Message queues, buffers. |
|
||||
| `page` | Documents, files. |
|
||||
| `package` | Packages, modules, crates. |
|
||||
| `sql_table` | Database tables, ECS component schemas. |
|
||||
| `class` | Class diagrams, ECS system definitions. |
|
||||
| `code` | Code blocks (set `language` property). |
|
||||
| `markdown` | Rich text blocks. |
|
||||
|
||||
## SQL Tables
|
||||
|
||||
```d2
|
||||
entity: {
|
||||
shape: sql_table
|
||||
id: u64 {constraint: primary_key}
|
||||
name: String
|
||||
position: Vec2
|
||||
faction_id: u64 {constraint: foreign_key}
|
||||
}
|
||||
```
|
||||
|
||||
## Class Diagrams
|
||||
|
||||
```d2
|
||||
perception_system: {
|
||||
shape: class
|
||||
+run(world: &mut World)
|
||||
-calculate_los(entity: Entity): HashSet<Entity>
|
||||
#update_knowledge(entity: Entity, seen: HashSet<Entity>)
|
||||
}
|
||||
```
|
||||
|
||||
## Sequence Diagrams
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
client: Godot Client
|
||||
server: Rust Server
|
||||
|
||||
client -> server: TickRequest(delta)
|
||||
server -> server: run ECS systems
|
||||
server -> client: WorldState(entities)
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
```d2
|
||||
node: Label {
|
||||
style: {
|
||||
fill: "#2d3436"
|
||||
stroke: "#333340"
|
||||
stroke-width: 2
|
||||
stroke-dash: 5 # Dashed line
|
||||
opacity: 0.8
|
||||
font-size: 14
|
||||
font-color: "#c8d0e0"
|
||||
bold: true
|
||||
italic: false
|
||||
border-radius: 4
|
||||
shadow: true
|
||||
3d: true # Rectangles only
|
||||
multiple: true # Stacked appearance
|
||||
double-border: true # Rectangles/ovals only
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Edge styling
|
||||
|
||||
```d2
|
||||
a -> b: {
|
||||
style: {
|
||||
stroke: "#c8d8f0"
|
||||
stroke-width: 2
|
||||
stroke-dash: 5
|
||||
opacity: 0.8
|
||||
animated: true # Animated flow
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-bg: "#1a1e24"
|
||||
color-stroke: "#333340"
|
||||
color-text: "#c8d0e0"
|
||||
color-accent: "#c8d8f0"
|
||||
}
|
||||
|
||||
node: {
|
||||
style.fill: ${color-bg}
|
||||
style.stroke: ${color-stroke}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
```
|
||||
|
||||
## Direction
|
||||
|
||||
```d2
|
||||
direction: right # left-to-right (default for dagre)
|
||||
direction: down # top-to-bottom
|
||||
direction: up
|
||||
direction: left
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
```d2
|
||||
...@shared-defs.d2 # Spread import (inline all definitions)
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
```d2
|
||||
node: Label {
|
||||
icon: https://icons.terrastruct.com/essentials/time.svg
|
||||
}
|
||||
```
|
||||
|
||||
## Layers (multi-board)
|
||||
|
||||
```d2
|
||||
# Base diagram content here
|
||||
|
||||
layers: {
|
||||
detailed: {
|
||||
# More detailed view
|
||||
}
|
||||
simplified: {
|
||||
# Simplified overview
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scenarios (animated transitions)
|
||||
|
||||
```d2
|
||||
# Base state
|
||||
|
||||
scenarios: {
|
||||
alert: {
|
||||
# Changes from base for alert state
|
||||
}
|
||||
combat: {
|
||||
# Changes from base for combat state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
```d2
|
||||
# This is a comment
|
||||
node: Label # Inline comment
|
||||
```
|
||||
|
||||
## Project Colors (from visual-grammar-v01.md)
|
||||
|
||||
| Constant | Hex | Usage |
|
||||
|----------|-----|-------|
|
||||
| Zone 1 floor | `#1a1e24` | Dark backgrounds |
|
||||
| Zone 1 wall | `#2a3040` | Component fill |
|
||||
| Outline standard | `#333340` | Borders, strokes |
|
||||
| Insert chrome | `#c8d0e0` | Text, labels |
|
||||
| Zone 1 fixture | `#c8d8f0` | Accents, highlights |
|
||||
@@ -0,0 +1,251 @@
|
||||
# Diagram Templates
|
||||
|
||||
Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Diagram
|
||||
|
||||
System components, relationships, communication channels.
|
||||
|
||||
**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture.
|
||||
|
||||
**Agents:** Tyre (system architecture), Qatux (architecture decision records).
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-bg: "#2a3040"
|
||||
color-stroke: "#333340"
|
||||
color-text: "#c8d0e0"
|
||||
color-accent: "#c8d8f0"
|
||||
}
|
||||
|
||||
direction: right
|
||||
|
||||
client: Godot Client {
|
||||
shape: hexagon
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
rendering: Rendering {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
ui: UI Layer {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
bridge: IPC Bridge {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
style.stroke: ${color-accent}
|
||||
}
|
||||
}
|
||||
|
||||
server: Rust Server {
|
||||
shape: hexagon
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
ecs: bevy_ecs {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
perception: Perception {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
bridge: IPC Bridge {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
style.stroke: ${color-accent}
|
||||
}
|
||||
}
|
||||
|
||||
client.bridge -> server.bridge: "stdin/stdout" {
|
||||
style.stroke: ${color-accent}
|
||||
style.stroke-dash: 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Entity Relationship
|
||||
|
||||
Data schemas, ECS components, knowledge graph structure.
|
||||
|
||||
**When to use:** Database tables, component definitions, entity relationships, knowledge store schema.
|
||||
|
||||
**Agents:** Tyre (ECS component design), Qatux (schema documentation).
|
||||
|
||||
```d2
|
||||
entity: Entity {
|
||||
shape: sql_table
|
||||
id: u64 {constraint: primary_key}
|
||||
name: String
|
||||
faction_id: u64 {constraint: foreign_key}
|
||||
}
|
||||
|
||||
position: Position {
|
||||
shape: sql_table
|
||||
entity_id: u64 {constraint: foreign_key}
|
||||
x: f32
|
||||
y: f32
|
||||
chunk_id: u32
|
||||
}
|
||||
|
||||
knowledge: KnowledgeEntry {
|
||||
shape: sql_table
|
||||
observer_id: u64 {constraint: foreign_key}
|
||||
subject_id: u64 {constraint: foreign_key}
|
||||
fact_type: FactType
|
||||
confidence: f32
|
||||
last_seen_tick: u64
|
||||
}
|
||||
|
||||
entity.id -> position.entity_id
|
||||
entity.id -> knowledge.observer_id
|
||||
entity.id -> knowledge.subject_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Sequence / Data Flow
|
||||
|
||||
Ordered interactions between systems over time.
|
||||
|
||||
**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges.
|
||||
|
||||
**Agents:** Tyre (system interaction design), Qatux (protocol documentation).
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
client: Godot Client
|
||||
bridge: IPC Bridge
|
||||
server: Rust Server
|
||||
ecs: ECS Systems
|
||||
|
||||
client -> bridge: TickRequest(delta, input)
|
||||
bridge -> server: deserialize + dispatch
|
||||
server -> ecs: run_systems(delta)
|
||||
ecs -> ecs: perception, AI, physics
|
||||
ecs -> server: collect WorldState
|
||||
server -> bridge: serialize WorldState
|
||||
bridge -> client: WorldState(entities, events)
|
||||
client -> client: update rendering
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. State Machine
|
||||
|
||||
Entity states, transitions, conditions.
|
||||
|
||||
**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases.
|
||||
|
||||
**Agents:** Tyre (behavior system design), Qatux (state documentation).
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-state: "#2a3040"
|
||||
color-text: "#c8d0e0"
|
||||
color-edge: "#c8d8f0"
|
||||
color-decision: "#333340"
|
||||
}
|
||||
|
||||
idle: Idle {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
alert: Alert {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
investigate: Investigate {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
combat: Combat {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
style.stroke: "#f0b840"
|
||||
}
|
||||
|
||||
flee: Flee {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
idle -> alert: "stimulus detected" { style.stroke: ${color-edge} }
|
||||
alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} }
|
||||
alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" }
|
||||
investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
combat -> flee: "health < threshold" { style.stroke: "#f0b840" }
|
||||
combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. UI Flow
|
||||
|
||||
Screen navigation, component hierarchy, interaction paths.
|
||||
|
||||
**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions.
|
||||
|
||||
**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation).
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-screen: "#1a1e24"
|
||||
color-panel: "#2a3040"
|
||||
color-text: "#c8d0e0"
|
||||
color-nav: "#c8d8f0"
|
||||
}
|
||||
|
||||
gameplay: Gameplay {
|
||||
style.fill: ${color-screen}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
hud: HUD {
|
||||
style.fill: ${color-panel}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
minimap: Minimap
|
||||
monologue: Monologue Panel
|
||||
insert_display: Insert Display
|
||||
}
|
||||
|
||||
world: World View {
|
||||
style.fill: ${color-panel}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
}
|
||||
|
||||
pause: Pause Menu {
|
||||
style.fill: ${color-screen}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
inventory: Inventory
|
||||
journal: Journal
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
dialogue: Dialogue Mode {
|
||||
style.fill: ${color-screen}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
speaker: Speaker Panel
|
||||
responses: Response List
|
||||
}
|
||||
|
||||
gameplay -> pause: "ESC" { style.stroke: ${color-nav} }
|
||||
pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||
gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} }
|
||||
dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||
RENDER="$SCRIPT_DIR/d2-render.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [directory] [options]
|
||||
|
||||
Batch render all .d2 files in a directory. Skips files whose PNG is
|
||||
newer than the source unless --force is used.
|
||||
|
||||
Options:
|
||||
--dry-run List files that would be rendered
|
||||
--force Re-render even if SVG is up to date
|
||||
--theme N Override theme for all files
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # All in docs/diagrams/
|
||||
$(basename "$0") docs/diagrams/architecture/ # One category
|
||||
$(basename "$0") --dry-run # Preview
|
||||
$(basename "$0") --force # Re-render everything
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
DIR="$REPO_ROOT/docs/diagrams"
|
||||
DRY_RUN=false
|
||||
FORCE=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--theme) EXTRA_ARGS+=(--theme "$2"); shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
*)
|
||||
if [[ -d "$1" ]] || [[ -d "$REPO_ROOT/$1" ]]; then
|
||||
DIR="$1"
|
||||
[[ "$DIR" != /* ]] && DIR="$REPO_ROOT/$DIR"
|
||||
else
|
||||
echo "Unknown option or directory: $1" >&2; exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; }
|
||||
|
||||
RENDERED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
while IFS= read -r -d '' d2_file; do
|
||||
png_file="${d2_file%.d2}.png"
|
||||
|
||||
# Skip if PNG is newer than source (unless --force)
|
||||
if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
rel_path="${d2_file#"$REPO_ROOT/"}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "Would render: $rel_path"
|
||||
RENDERED=$((RENDERED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then
|
||||
RENDERED=$((RENDERED + 1))
|
||||
else
|
||||
echo "FAILED: $rel_path" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done < <(find "$DIR" -name '*.d2' -print0 | sort -z)
|
||||
|
||||
echo ""
|
||||
echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed"
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
D2="/home/linuxbrew/.linuxbrew/bin/d2"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||
|
||||
DEFAULT_THEME=200
|
||||
DEFAULT_LAYOUT="dagre"
|
||||
DEFAULT_PAD=100
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [validate|fmt] <file.d2> [options]
|
||||
|
||||
Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout).
|
||||
|
||||
Commands:
|
||||
validate <file> Check syntax without rendering
|
||||
fmt <file> Auto-format in place
|
||||
|
||||
Options:
|
||||
--theme N Override theme (default: $DEFAULT_THEME)
|
||||
--layout NAME Override layout engine (default: $DEFAULT_LAYOUT)
|
||||
--sketch Enable hand-drawn sketch mode
|
||||
--output PATH Override output path (default: input with .png extension)
|
||||
--svg Render to SVG instead of PNG
|
||||
|
||||
Examples:
|
||||
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2
|
||||
$(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2
|
||||
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
# Parse subcommand
|
||||
SUBCMD=""
|
||||
case "$1" in
|
||||
validate|fmt)
|
||||
SUBCMD="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
INPUT="$1"
|
||||
shift
|
||||
|
||||
# Resolve to absolute path
|
||||
[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT"
|
||||
|
||||
[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; }
|
||||
|
||||
# Handle subcommands
|
||||
if [[ -n "$SUBCMD" ]]; then
|
||||
"$D2" "$SUBCMD" "$INPUT"
|
||||
echo "OK: $SUBCMD $INPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse render options
|
||||
THEME="$DEFAULT_THEME"
|
||||
LAYOUT="$DEFAULT_LAYOUT"
|
||||
SKETCH=""
|
||||
OUTPUT=""
|
||||
FORMAT="png"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--theme) THEME="$2"; shift 2 ;;
|
||||
--layout) LAYOUT="$2"; shift 2 ;;
|
||||
--sketch) SKETCH="-s"; shift ;;
|
||||
--output) OUTPUT="$2"; shift 2 ;;
|
||||
--svg) FORMAT="svg"; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Derive output path
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT="${INPUT%.d2}.$FORMAT"
|
||||
fi
|
||||
|
||||
# Render
|
||||
"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT"
|
||||
|
||||
SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
|
||||
echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)"
|
||||
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: frame0-wireframe
|
||||
description: >
|
||||
Create and export UI wireframes using Frame0 (local desktop wireframing
|
||||
app with HTTP API). Use when the user says "create wireframe", "wireframe
|
||||
this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD",
|
||||
"Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe.
|
||||
Wireframes are authored as local JSON files (source of truth) and synced
|
||||
to Frame0 for rendering and export. Requires Frame0 to be running locally.
|
||||
---
|
||||
|
||||
# Frame0 Wireframe Generation
|
||||
|
||||
Create UI wireframes as JSON files, sync them to Frame0 for rendering, and
|
||||
export as PNG. Local JSON is the source of truth — Frame0 is the renderer.
|
||||
|
||||
**Frame0 is a renderer, not a workspace.** Treat it as disposable output.
|
||||
Push freely, delete test pages, keep it clean. Never pull from Frame0 unless
|
||||
the user explicitly says they have made edits in Frame0 and want to import
|
||||
them. The pull workflow exists for that case only — do not use it proactively.
|
||||
|
||||
**Prerequisite:** Frame0 desktop app must be running. If not available,
|
||||
stop and inform the user. Point to `references/setup-guide.md`.
|
||||
|
||||
## Health Check
|
||||
|
||||
Always check first:
|
||||
|
||||
```bash
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||
```
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Health check** — verify Frame0 is running
|
||||
2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json`
|
||||
3. **Push to Frame0** — `frame0-sync.py push <file.json>`
|
||||
4. **Export PNG** — `frame0-sync.py export <file.json> <output.png>`
|
||||
5. **Clean up** — delete test/scratch pages from Frame0 when done
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 |
|
||||
| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands |
|
||||
|
||||
## Wireframe JSON Format
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Dialogue Box",
|
||||
"shapes": {
|
||||
"panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||
"fillColor": "#1a1e24",
|
||||
"strokeColor": "#333340",
|
||||
"corners": [8, 8, 8, 8]
|
||||
},
|
||||
"speaker": {
|
||||
"type": "Text",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 520,
|
||||
"text": "LERA KONSTANTIN",
|
||||
"fontColor": "#c8d0e0",
|
||||
"fontSize": 16
|
||||
},
|
||||
"btn-ask": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||
"fillColor": "#2a3040",
|
||||
"strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
"flow-1": {
|
||||
"tailId": "panel",
|
||||
"headId": "btn-ask",
|
||||
"strokeColor": "#c8d8f0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key rules
|
||||
|
||||
- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`)
|
||||
- **`parent`** references another local shape ID for nesting
|
||||
- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line`
|
||||
- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`)
|
||||
- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle,
|
||||
theme tokens for colors). The sync script handles the mapping transparently.
|
||||
- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID
|
||||
|
||||
### Sync commands
|
||||
|
||||
```bash
|
||||
SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py"
|
||||
|
||||
# Push local JSON to Frame0 (clears page, recreates all shapes)
|
||||
$SYNC push docs/design/wireframes/dialogue/dialogue-box.json
|
||||
|
||||
# Pull Frame0 page back to local JSON (preserves local IDs via mapping)
|
||||
$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json
|
||||
|
||||
# Push + export as PNG in one step
|
||||
$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \
|
||||
docs/design/wireframes/dialogue/dialogue-box.png
|
||||
```
|
||||
|
||||
### Batch export
|
||||
|
||||
Use this when exporting multiple wireframes. It runs as a single Bash call,
|
||||
avoiding repeated permission prompts.
|
||||
|
||||
```bash
|
||||
BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh"
|
||||
|
||||
# Dry run first — shows full manifest, no Frame0 calls
|
||||
$BATCH --dry-run
|
||||
|
||||
# Export everything (skips PNGs already newer than their JSON)
|
||||
$BATCH
|
||||
|
||||
# Export one category only
|
||||
$BATCH --category dialogue
|
||||
|
||||
# Force re-export of everything
|
||||
$BATCH --force
|
||||
```
|
||||
|
||||
**Always dry-run first, then get approval before running the live export.**
|
||||
|
||||
## Low-Level Commands
|
||||
|
||||
For ad-hoc operations or exec calls not covered by sync:
|
||||
|
||||
```bash
|
||||
CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh"
|
||||
$CMD health
|
||||
$CMD list-pages
|
||||
$CMD current-page
|
||||
$CMD get-page <page-id>
|
||||
$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||
$CMD create-connector <tail-id> <head-id>
|
||||
$CMD move <shape-id> <dx> <dy>
|
||||
$CMD export --format image/png
|
||||
$CMD exec "view:fit-to-screen" '{}'
|
||||
```
|
||||
|
||||
If you find yourself using `exec` for the same command repeatedly, flag it as
|
||||
a candidate for a proper wrapper in `frame0-cmd.sh`.
|
||||
|
||||
## Project Styling Defaults
|
||||
|
||||
Colors from `docs/design/visual-grammar-v01.md`:
|
||||
|
||||
| Role | Hex | Frame0 token |
|
||||
|------|-----|-------------|
|
||||
| Background | `#1a1e24` | `$sage3` |
|
||||
| Stroke | `#333340` | `$slate6` |
|
||||
| Fill | `#2a3040` | `$slate5` |
|
||||
| Text | `#c8d0e0` | `$mint12` |
|
||||
| Accent | `#c8d8f0` | `$blue12` |
|
||||
|
||||
Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push.
|
||||
|
||||
## Output Convention
|
||||
|
||||
```
|
||||
docs/design/wireframes/
|
||||
hud/ # HUD layout wireframes
|
||||
menus/ # Menu screen wireframes
|
||||
dialogue/ # Dialogue box wireframes
|
||||
insert/ # Neural insert wireframes
|
||||
```
|
||||
|
||||
Both `.json` source and `.png` exports are tracked in git.
|
||||
`.idmap.json` mapping files are gitignored.
|
||||
|
||||
## Agent Guidance
|
||||
|
||||
- **Araminta** — Primary user. Full wireframe creation, layout iteration,
|
||||
visual consistency. Uses all component library patterns.
|
||||
- **Tyre** — Interface architecture wireframes. System boundary diagrams.
|
||||
- **Qatux** — Export wireframes for UI decision records and documentation.
|
||||
|
||||
## References
|
||||
|
||||
- `references/component-library.md` — Pre-built JSON wireframe templates
|
||||
(HUD, dialogue, menus, modals, lists, inventory). Copy and adapt.
|
||||
- `references/api-reference.md` — Frame0 HTTP API command reference and
|
||||
type mappings. Read for low-level control.
|
||||
- `references/setup-guide.md` — Frame0 installation and startup for Fedora.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Frame0 HTTP API Reference
|
||||
|
||||
Frame0 exposes a local HTTP API when the desktop app is running.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
POST http://localhost:{port}/execute_command
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag).
|
||||
|
||||
## Request / Response
|
||||
|
||||
```json
|
||||
{"command": "namespace:action", "args": { ... }}
|
||||
```
|
||||
|
||||
```json
|
||||
{"success": true, "data": { ... }}
|
||||
{"success": false, "error": "description"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Mapping
|
||||
|
||||
Frame0 uses different type names for create vs get:
|
||||
|
||||
| Create API (`type`) | Get API (internal) | Description |
|
||||
|--------------------|--------------------|-------------|
|
||||
| `Rectangle` | `Box` | Rectangle with optional corners |
|
||||
| `Ellipse` | `Ellipse` | Circle/ellipse |
|
||||
| `Text` | `Text` | Text label |
|
||||
| `Line` | `Line` | Line/polyline |
|
||||
| `Frame` | `Frame` | Container from library |
|
||||
| `Freehand` | `Freehand` | Freehand drawing |
|
||||
| `Highlighter` | `Highlighter` | Highlighter stroke |
|
||||
|
||||
The sync script handles this mapping transparently.
|
||||
|
||||
## Color Tokens
|
||||
|
||||
Frame0 maps hex colors to theme tokens on creation (`convertColors: true`):
|
||||
|
||||
| Hex | Token | Role |
|
||||
|-----|-------|------|
|
||||
| `#1a1e24` | `$sage3` | Background |
|
||||
| `#2a3040` | `$slate5` | Fill |
|
||||
| `#333340` | `$slate6` | Stroke |
|
||||
| `#c8d0e0` | `$mint12` | Text |
|
||||
| `#c8d8f0` | `$blue12` | Accent |
|
||||
|
||||
Both hex and token strings work in the API. Tokens are preserved on round-trip.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### shape:create-shape
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:create-shape",
|
||||
"args": {
|
||||
"type": "Rectangle",
|
||||
"shapeProps": {
|
||||
"name": "my-button",
|
||||
"left": 100, "top": 200, "width": 120, "height": 36,
|
||||
"fillColor": "#2a3040",
|
||||
"strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"parentId": "optional-parent-shape-id",
|
||||
"convertColors": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returns: shape ID (string).
|
||||
|
||||
### shape:get-shape
|
||||
|
||||
```json
|
||||
{"command": "shape:get-shape", "args": {"shapeId": "id"}}
|
||||
```
|
||||
|
||||
### shape:update-shape
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:update-shape",
|
||||
"args": {
|
||||
"shapeId": "id",
|
||||
"shapeProps": {"fillColor": "#1a1e24", "text": "Updated"},
|
||||
"convertColors": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### shape:move
|
||||
|
||||
```json
|
||||
{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}}
|
||||
```
|
||||
|
||||
### shape:create-connector
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:create-connector",
|
||||
"args": {
|
||||
"tailId": "source-id",
|
||||
"headId": "target-id",
|
||||
"shapeProps": {"strokeColor": "#c8d8f0"},
|
||||
"convertColors": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### shape:create-icon
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:create-icon",
|
||||
"args": {
|
||||
"iconName": "search",
|
||||
"shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### shape:get-available-icons
|
||||
|
||||
```json
|
||||
{"command": "shape:get-available-icons", "args": {}}
|
||||
```
|
||||
|
||||
### shape:group / shape:ungroup
|
||||
|
||||
```json
|
||||
{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||
{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}}
|
||||
```
|
||||
|
||||
### edit:delete / edit:duplicate
|
||||
|
||||
```json
|
||||
{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||
{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}}
|
||||
```
|
||||
|
||||
### page:add
|
||||
|
||||
```json
|
||||
{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}}
|
||||
```
|
||||
|
||||
Returns: `{id, type, name}`.
|
||||
|
||||
### page:get
|
||||
|
||||
```json
|
||||
{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}}
|
||||
```
|
||||
|
||||
### page:get-current-page
|
||||
|
||||
```json
|
||||
{"command": "page:get-current-page", "args": {}}
|
||||
```
|
||||
|
||||
Returns: page ID string.
|
||||
|
||||
### page:set-current-page
|
||||
|
||||
```json
|
||||
{"command": "page:set-current-page", "args": {"pageId": "id"}}
|
||||
```
|
||||
|
||||
### doc:get (list all pages)
|
||||
|
||||
```json
|
||||
{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}}
|
||||
```
|
||||
|
||||
### page:delete
|
||||
|
||||
```json
|
||||
{"command": "page:delete", "args": {"pageId": "id"}}
|
||||
```
|
||||
|
||||
### file:export-image
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "file:export-image",
|
||||
"args": {
|
||||
"pageId": "optional-page-id",
|
||||
"format": "image/png",
|
||||
"fillBackground": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
|
||||
Returns: base64-encoded image data.
|
||||
|
||||
### view:fit-to-screen
|
||||
|
||||
```json
|
||||
{"command": "view:fit-to-screen", "args": {}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shape Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
|----------|------|-------|
|
||||
| `name` | string | Shape identifier/label |
|
||||
| `left` | number | X position (origin: top-left) |
|
||||
| `top` | number | Y position |
|
||||
| `width` | number | Width in pixels |
|
||||
| `height` | number | Height in pixels |
|
||||
| `fillColor` | string | Hex or `$token` |
|
||||
| `strokeColor` | string | Hex or `$token` |
|
||||
| `strokeWidth` | number | Border width |
|
||||
| `fontColor` | string | Text color (hex or `$token`) |
|
||||
| `fontSize` | number | Font size in pixels |
|
||||
| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) |
|
||||
| `text` | string | Text content |
|
||||
| `wordWrap` | boolean | Enable word wrapping |
|
||||
| `corners` | number[4] | Border radius [TL, TR, BR, BL] |
|
||||
| `roughness` | number | Sketch roughness (Frame0 default: 1) |
|
||||
| `constraints` | array | Auto-sizing constraints |
|
||||
| `horzAlign` | string | Horizontal text alignment |
|
||||
| `vertAlign` | string | Vertical text alignment |
|
||||
| `fillStyle` | string | Fill style (`none` for transparent) |
|
||||
| `path` | array | Coordinate pairs for lines |
|
||||
@@ -0,0 +1,467 @@
|
||||
# Component Library
|
||||
|
||||
Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON,
|
||||
adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push.
|
||||
|
||||
**Viewport:** 1140x780 (Godot project settings)
|
||||
**Grid unit:** 8px
|
||||
**Min touch target:** 36px height
|
||||
**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title)
|
||||
|
||||
---
|
||||
|
||||
## 1. HUD Layout
|
||||
|
||||
Main gameplay overlay. Minimap top-right, monologue bottom-center,
|
||||
insert display bottom-left, action hints bottom-right.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "HUD Layout",
|
||||
"shapes": {
|
||||
"minimap": {
|
||||
"type": "Rectangle",
|
||||
"left": 880, "top": 20, "width": 240, "height": 240,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"minimap-label": {
|
||||
"type": "Text",
|
||||
"parent": "minimap",
|
||||
"left": 890, "top": 30,
|
||||
"text": "Minimap",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"monologue": {
|
||||
"type": "Rectangle",
|
||||
"left": 300, "top": 680, "width": 520, "height": 80,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"monologue-text": {
|
||||
"type": "Text",
|
||||
"parent": "monologue",
|
||||
"left": 310, "top": 700, "width": 500,
|
||||
"text": "Internal monologue text appears here...",
|
||||
"fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true
|
||||
},
|
||||
"insert": {
|
||||
"type": "Rectangle",
|
||||
"left": 20, "top": 600, "width": 260, "height": 160,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"insert-label": {
|
||||
"type": "Text",
|
||||
"parent": "insert",
|
||||
"left": 30, "top": 620,
|
||||
"text": "Neural Insert Data",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"hints": {
|
||||
"type": "Rectangle",
|
||||
"left": 880, "top": 700, "width": 240, "height": 60,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"hints-label": {
|
||||
"type": "Text",
|
||||
"parent": "hints",
|
||||
"left": 890, "top": 720,
|
||||
"text": "[E] Interact [TAB] Insert",
|
||||
"fontColor": "#c8d0e0", "fontSize": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Dialogue Box
|
||||
|
||||
Speaker panel with response options. Anchored bottom-center during dialogue mode.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Dialogue Box",
|
||||
"shapes": {
|
||||
"panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [8, 8, 8, 8]
|
||||
},
|
||||
"speaker": {
|
||||
"type": "Text",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 520,
|
||||
"text": "LERA KONSTANTIN",
|
||||
"fontColor": "#c8d0e0", "fontSize": 16
|
||||
},
|
||||
"text-area": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 550, "width": 760, "height": 100,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"dialogue-text": {
|
||||
"type": "Text",
|
||||
"parent": "text-area",
|
||||
"left": 200, "top": 560, "width": 740,
|
||||
"text": "You look like you could use a drink. First time on the station?",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||
},
|
||||
"btn-option1": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-option1-label": {
|
||||
"type": "Text",
|
||||
"parent": "btn-option1",
|
||||
"left": 200, "top": 674,
|
||||
"text": "[1] Ask about the station",
|
||||
"fontColor": "#c8d8f0", "fontSize": 12
|
||||
},
|
||||
"btn-option2": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 710, "width": 370, "height": 30,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-option2-label": {
|
||||
"type": "Text",
|
||||
"parent": "btn-option2",
|
||||
"left": 200, "top": 714,
|
||||
"text": "[2] Ask about recent events",
|
||||
"fontColor": "#c8d8f0", "fontSize": 12
|
||||
},
|
||||
"btn-leave": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 580, "top": 670, "width": 180, "height": 30,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-leave-label": {
|
||||
"type": "Text",
|
||||
"parent": "btn-leave",
|
||||
"left": 590, "top": 674,
|
||||
"text": "[3] Leave",
|
||||
"fontColor": "#c8d8f0", "fontSize": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Menu Screen
|
||||
|
||||
Full-screen menu with sidebar navigation and content area.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Pause Menu",
|
||||
"shapes": {
|
||||
"bg": {
|
||||
"type": "Rectangle",
|
||||
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||
"fillColor": "#1a1e24"
|
||||
},
|
||||
"nav": {
|
||||
"type": "Rectangle",
|
||||
"parent": "bg",
|
||||
"left": 20, "top": 20, "width": 200, "height": 740,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-inventory": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 40, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-inventory-label": {
|
||||
"type": "Text", "parent": "btn-inventory",
|
||||
"left": 40, "top": 48, "text": "Inventory",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-journal": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 86, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-journal-label": {
|
||||
"type": "Text", "parent": "btn-journal",
|
||||
"left": 40, "top": 94, "text": "Journal",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-map": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 132, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-map-label": {
|
||||
"type": "Text", "parent": "btn-map",
|
||||
"left": 40, "top": 140, "text": "Map",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-settings": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 178, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-settings-label": {
|
||||
"type": "Text", "parent": "btn-settings",
|
||||
"left": 40, "top": 186, "text": "Settings",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-resume": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 720, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-resume-label": {
|
||||
"type": "Text", "parent": "btn-resume",
|
||||
"left": 40, "top": 728, "text": "Resume",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"content": {
|
||||
"type": "Rectangle",
|
||||
"parent": "bg",
|
||||
"left": 240, "top": 20, "width": 880, "height": 740,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"content-label": {
|
||||
"type": "Text", "parent": "content",
|
||||
"left": 260, "top": 40,
|
||||
"text": "Content area",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Modal Dialog
|
||||
|
||||
Centered overlay for confirmations, alerts, choices.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Modal Dialog",
|
||||
"shapes": {
|
||||
"overlay": {
|
||||
"type": "Rectangle",
|
||||
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||
"fillColor": "#0a0c10"
|
||||
},
|
||||
"modal": {
|
||||
"type": "Rectangle",
|
||||
"parent": "overlay",
|
||||
"left": 320, "top": 240, "width": 500, "height": 300,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [8, 8, 8, 8]
|
||||
},
|
||||
"title": {
|
||||
"type": "Text", "parent": "modal",
|
||||
"left": 340, "top": 260,
|
||||
"text": "Confirm Action",
|
||||
"fontColor": "#c8d0e0", "fontSize": 18
|
||||
},
|
||||
"divider": {
|
||||
"type": "Line", "parent": "modal",
|
||||
"left": 340, "top": 290, "width": 460, "height": 0,
|
||||
"strokeColor": "#333340"
|
||||
},
|
||||
"body-1": {
|
||||
"type": "Text", "parent": "modal",
|
||||
"left": 340, "top": 310,
|
||||
"text": "Are you sure you want to proceed?",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"body-2": {
|
||||
"type": "Text", "parent": "modal",
|
||||
"left": 340, "top": 340,
|
||||
"text": "This action cannot be undone.",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"btn-cancel": {
|
||||
"type": "Rectangle", "parent": "modal",
|
||||
"left": 480, "top": 480, "width": 120, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-cancel-label": {
|
||||
"type": "Text", "parent": "btn-cancel",
|
||||
"left": 510, "top": 488,
|
||||
"text": "Cancel",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-confirm": {
|
||||
"type": "Rectangle", "parent": "modal",
|
||||
"left": 620, "top": 480, "width": 120, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-confirm-label": {
|
||||
"type": "Text", "parent": "btn-confirm",
|
||||
"left": 645, "top": 488,
|
||||
"text": "Confirm",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. List View
|
||||
|
||||
Scrollable list with item selection and detail panel.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "List View",
|
||||
"shapes": {
|
||||
"list-panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 20, "top": 20, "width": 400, "height": 740,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-1": {
|
||||
"type": "Rectangle", "parent": "list-panel",
|
||||
"left": 30, "top": 30, "width": 380, "height": 40,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-1-label": {
|
||||
"type": "Text", "parent": "item-1",
|
||||
"left": 40, "top": 38, "text": "Item Alpha",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"item-2": {
|
||||
"type": "Rectangle", "parent": "list-panel",
|
||||
"left": 30, "top": 80, "width": 380, "height": 40,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-2-label": {
|
||||
"type": "Text", "parent": "item-2",
|
||||
"left": 40, "top": 88, "text": "Item Beta",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"item-3": {
|
||||
"type": "Rectangle", "parent": "list-panel",
|
||||
"left": 30, "top": 130, "width": 380, "height": 40,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-3-label": {
|
||||
"type": "Text", "parent": "item-3",
|
||||
"left": 40, "top": 138, "text": "Item Gamma",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"detail-panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 440, "top": 20, "width": 680, "height": 740,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"detail-title": {
|
||||
"type": "Text", "parent": "detail-panel",
|
||||
"left": 460, "top": 40,
|
||||
"text": "Item Alpha",
|
||||
"fontColor": "#c8d0e0", "fontSize": 18
|
||||
},
|
||||
"detail-body": {
|
||||
"type": "Text", "parent": "detail-panel",
|
||||
"left": 460, "top": 80, "width": 640,
|
||||
"text": "Description and properties appear here.",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Inventory Grid
|
||||
|
||||
Grid of cells for item management.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Inventory Grid",
|
||||
"shapes": {
|
||||
"panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 240, "top": 100, "width": 660, "height": 580,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
|
||||
},
|
||||
"title": {
|
||||
"type": "Text", "parent": "panel",
|
||||
"left": 260, "top": 120,
|
||||
"text": "INVENTORY",
|
||||
"fontColor": "#c8d0e0", "fontSize": 18
|
||||
},
|
||||
"cell-1-1": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 260, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-1-2": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 332, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-1-3": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 404, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-1-4": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 476, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-1": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 260, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-2": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 332, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-3": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 404, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-4": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 476, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"detail": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 580, "top": 160, "width": 300, "height": 400,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"detail-title": {
|
||||
"type": "Text", "parent": "detail",
|
||||
"left": 600, "top": 180,
|
||||
"text": "Selected Item Name",
|
||||
"fontColor": "#c8d0e0", "fontSize": 16
|
||||
},
|
||||
"detail-body": {
|
||||
"type": "Text", "parent": "detail",
|
||||
"left": 600, "top": 210, "width": 260,
|
||||
"text": "Item description and stats",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# Frame0 Setup Guide
|
||||
|
||||
## Installation (Fedora)
|
||||
|
||||
Download from https://frame0.app/download and install the RPM:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./frame0-*.x86_64.rpm
|
||||
```
|
||||
|
||||
Requires: Fedora 40 or later (x86_64).
|
||||
|
||||
## Starting Frame0
|
||||
|
||||
Launch the desktop application:
|
||||
|
||||
```bash
|
||||
frame0 &
|
||||
```
|
||||
|
||||
Frame0 exposes an HTTP API at `localhost:58320` when running.
|
||||
|
||||
## Verify API Access
|
||||
|
||||
```bash
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||
```
|
||||
|
||||
Expected output: `Frame0 is running on port 58320`
|
||||
|
||||
## Port Configuration
|
||||
|
||||
Default port: **58320**
|
||||
|
||||
To use a different port, set the environment variable:
|
||||
|
||||
```bash
|
||||
export FRAME0_PORT=58321
|
||||
```
|
||||
|
||||
Or pass `--port` to any script:
|
||||
|
||||
```bash
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| "Connection refused" | Frame0 not running | Start the desktop app |
|
||||
| "Port in use" | Another instance running | Close duplicate or use different port |
|
||||
| Script hangs | API unresponsive | Restart Frame0 |
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${FRAME0_PORT:-58320}"
|
||||
ENDPOINT="http://localhost:${PORT}/execute_command"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <command> [args...] [--port N]
|
||||
|
||||
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
|
||||
curl calls. Requires Frame0 desktop app to be running.
|
||||
|
||||
Commands:
|
||||
health Check if Frame0 is running
|
||||
exec <namespace:action> <json> Execute a raw API command
|
||||
create-shape <type> <json-props> Create a shape (Rectangle, Ellipse, Text, Line)
|
||||
get-shape <id> Get shape details
|
||||
update-shape <id> <json-props> Update shape properties
|
||||
delete <id> [id...] Delete shapes by ID
|
||||
move <id> <dx> <dy> Move a shape by pixel offset
|
||||
duplicate <id> Duplicate a shape
|
||||
group <id> [id...] Group shapes
|
||||
ungroup <group-id> Ungroup a group
|
||||
create-connector <tail-id> <head-id> [json-props] Connect two shapes
|
||||
create-icon <name> <json-props> Create an icon shape
|
||||
add-page <name> Add a new page (becomes current)
|
||||
get-page [page-id] Get current or specific page data
|
||||
list-pages [--shapes] List all pages (--shapes for shape data)
|
||||
current-page Get current page ID
|
||||
set-page <page-id> Set current page
|
||||
export [page-id] [--format mime] Export page as image (default: image/png)
|
||||
fit Fit view to screen
|
||||
|
||||
Options:
|
||||
--port N Frame0 API port (default: $PORT, env: FRAME0_PORT)
|
||||
|
||||
Examples:
|
||||
$(basename "$0") health
|
||||
$(basename "$0") add-page "HUD Layout"
|
||||
$(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||
$(basename "$0") list-pages
|
||||
$(basename "$0") export --format image/png
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse --port from anywhere in args
|
||||
ARGS=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
# Execute a Frame0 API command, return data or error
|
||||
frame0_exec() {
|
||||
local command="$1"
|
||||
local args
|
||||
args="${2:-"{}"}"
|
||||
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
|
||||
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
|
||||
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local http_code body
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != 2* ]]; then
|
||||
echo "ERROR: HTTP $http_code from Frame0" >&2
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse success/error from response
|
||||
python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
r = json.loads(sys.stdin.read())
|
||||
if r.get('success'):
|
||||
d = r.get('data')
|
||||
if d is not None:
|
||||
print(json.dumps(d, indent=2))
|
||||
else:
|
||||
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
" <<< "$body"
|
||||
}
|
||||
|
||||
# Build JSON array from remaining args
|
||||
ids_to_json_array() {
|
||||
local arr="["
|
||||
local first=true
|
||||
for id in "$@"; do
|
||||
[[ "$first" == true ]] && first=false || arr+=","
|
||||
arr+="\"$id\""
|
||||
done
|
||||
arr+="]"
|
||||
echo "$arr"
|
||||
}
|
||||
|
||||
CMD="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$CMD" in
|
||||
health)
|
||||
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
|
||||
echo "Frame0 is running on port $PORT"
|
||||
else
|
||||
echo "Frame0 is NOT running on port $PORT" >&2
|
||||
echo "Start Frame0 desktop app, then retry." >&2
|
||||
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
exec)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
|
||||
frame0_exec "$1" "$2"
|
||||
;;
|
||||
|
||||
create-shape)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; }
|
||||
local_type="$1"
|
||||
local_props="$2"
|
||||
local_parent="${3:-}"
|
||||
local_parent_arg=""
|
||||
[[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\""
|
||||
frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
get-shape)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: get-shape <id>" >&2; exit 1; }
|
||||
frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}"
|
||||
;;
|
||||
|
||||
update-shape)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; }
|
||||
frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
delete)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
|
||||
local_arr=$(ids_to_json_array "$@")
|
||||
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
|
||||
;;
|
||||
|
||||
move)
|
||||
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
|
||||
frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}"
|
||||
;;
|
||||
|
||||
duplicate)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: duplicate <id> [dx] [dy]" >&2; exit 1; }
|
||||
local_dx="${2:-0}"
|
||||
local_dy="${3:-0}"
|
||||
frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}"
|
||||
;;
|
||||
|
||||
group)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: group <id> <id> [id...]" >&2; exit 1; }
|
||||
local_arr=$(ids_to_json_array "$@")
|
||||
frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}"
|
||||
;;
|
||||
|
||||
ungroup)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: ungroup <group-id>" >&2; exit 1; }
|
||||
frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}"
|
||||
;;
|
||||
|
||||
create-connector)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; }
|
||||
local_props="${3:-{}}"
|
||||
frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
create-icon)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; }
|
||||
frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
add-page)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: add-page <name>" >&2; exit 1; }
|
||||
frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}"
|
||||
;;
|
||||
|
||||
get-page)
|
||||
if [[ $# -ge 1 ]]; then
|
||||
frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}"
|
||||
else
|
||||
local_id
|
||||
local_id=$(frame0_exec "page:get-current-page")
|
||||
# Strip quotes from returned ID
|
||||
local_id=$(echo "$local_id" | tr -d '"')
|
||||
frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}"
|
||||
fi
|
||||
;;
|
||||
|
||||
list-pages)
|
||||
local_shapes="false"
|
||||
[[ "${1:-}" == "--shapes" ]] && local_shapes="true"
|
||||
frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}"
|
||||
;;
|
||||
|
||||
current-page)
|
||||
frame0_exec "page:get-current-page"
|
||||
;;
|
||||
|
||||
set-page)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; }
|
||||
frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}"
|
||||
;;
|
||||
|
||||
export)
|
||||
local_page_id=""
|
||||
local_format="image/png"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--format) local_format="$2"; shift 2 ;;
|
||||
*) local_page_id="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
local_page_arg=""
|
||||
[[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", "
|
||||
frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}"
|
||||
;;
|
||||
|
||||
fit)
|
||||
frame0_exec "view:fit-to-screen"
|
||||
;;
|
||||
|
||||
--help|-h|help)
|
||||
usage
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command: $CMD" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# Batch export wireframes from JSON to PNG via Frame0.
|
||||
#
|
||||
# Finds all .json wireframe files under docs/design/wireframes/ and exports
|
||||
# each to a matching .png. Skips files whose PNG is already newer than the
|
||||
# JSON, unless --force is passed.
|
||||
#
|
||||
# Usage:
|
||||
# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR]
|
||||
#
|
||||
# Options:
|
||||
# --dry-run Print manifest only, don't touch Frame0.
|
||||
# --force Re-export even if PNG already exists and is up to date.
|
||||
# --category CAT Limit to one subdirectory (e.g. --category dialogue)
|
||||
# --root DIR Wireframes root dir (default: docs/design/wireframes)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 All exports succeeded (or nothing to do)
|
||||
# 1 One or more exports failed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
SYNC="$SCRIPT_DIR/frame0-sync.py"
|
||||
DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes"
|
||||
|
||||
DRY_RUN=false
|
||||
FORCE=false
|
||||
CATEGORY=""
|
||||
WF_ROOT="$DEFAULT_ROOT"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--category) CATEGORY="$2"; shift 2 ;;
|
||||
--root) WF_ROOT="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
sed -n '/^# /p' "$0" | sed 's/^# //'
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d "$WF_ROOT" ]]; then
|
||||
echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Collect JSON files, optionally filtered by category subdirectory
|
||||
mapfile -t JSON_FILES < <(
|
||||
if [[ -n "$CATEGORY" ]]; then
|
||||
find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort
|
||||
else
|
||||
find "$WF_ROOT" -name "*.json" ! -name ".*" | sort
|
||||
fi
|
||||
)
|
||||
|
||||
if [[ ${#JSON_FILES[@]} -eq 0 ]]; then
|
||||
echo "No wireframe JSON files found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Classify files into to-export and to-skip
|
||||
TO_EXPORT=()
|
||||
TO_SKIP=()
|
||||
|
||||
for json in "${JSON_FILES[@]}"; do
|
||||
png="${json%.json}.png"
|
||||
if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then
|
||||
TO_EXPORT+=("$json")
|
||||
else
|
||||
TO_SKIP+=("$json")
|
||||
fi
|
||||
done
|
||||
|
||||
# Print manifest
|
||||
if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Will export (${#TO_EXPORT[@]} files):"
|
||||
for json in "${TO_EXPORT[@]}"; do
|
||||
png="${json%.json}.png"
|
||||
rel="${json#$REPO_ROOT/}"
|
||||
if [[ ! -f "$png" ]]; then
|
||||
status="new"
|
||||
else
|
||||
status="updated"
|
||||
fi
|
||||
printf " [%-7s] %s\n" "$status" "$rel"
|
||||
done
|
||||
else
|
||||
echo ""
|
||||
echo "Nothing to export (all PNGs up to date)."
|
||||
fi
|
||||
|
||||
if [[ ${#TO_SKIP[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Will skip (${#TO_SKIP[@]} files already up to date):"
|
||||
for json in "${TO_SKIP[@]}"; do
|
||||
rel="${json#$REPO_ROOT/}"
|
||||
printf " [skip ] %s\n" "$rel"
|
||||
done
|
||||
fi
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo ""
|
||||
echo "Dry run — no exports performed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
FAILED_FILES=()
|
||||
|
||||
TOTAL=${#TO_EXPORT[@]}
|
||||
IDX=0
|
||||
|
||||
for json in "${TO_EXPORT[@]}"; do
|
||||
IDX=$((IDX + 1))
|
||||
png="${json%.json}.png"
|
||||
rel="${json#$REPO_ROOT/}"
|
||||
|
||||
printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel"
|
||||
|
||||
output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt)
|
||||
rc=$?
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true)
|
||||
echo "ok $size"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "FAILED"
|
||||
cat /tmp/frame0-batch-err.txt >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_FILES+=("$rel")
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "$PASSED exported, $FAILED failed."
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Failed:" >&2
|
||||
for f in "${FAILED_FILES[@]}"; do
|
||||
echo " $f" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back.
|
||||
|
||||
Local JSON is source of truth. Frame0 is a renderer.
|
||||
A mapping file tracks local_id <-> frame0_id across push/pull cycles.
|
||||
|
||||
Usage:
|
||||
frame0-sync.py push <wireframe.json> [--port PORT]
|
||||
frame0-sync.py pull <page-id|page-name> <output.json> [--port PORT]
|
||||
frame0-sync.py export <wireframe.json> <output.png> [--port PORT] [--format MIME]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
DEFAULT_PORT = 58320
|
||||
|
||||
|
||||
def api(port, command, args=None):
|
||||
"""Execute a Frame0 API command. Returns the data field on success."""
|
||||
url = f"http://localhost:{port}/execute_command"
|
||||
payload = json.dumps({"command": command, "args": args or {}}).encode()
|
||||
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
result = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not result.get("success"):
|
||||
print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return result.get("data")
|
||||
|
||||
|
||||
# -- Mapping file management --------------------------------------------------
|
||||
|
||||
def mapping_path(wireframe_path):
|
||||
"""Derive the idmap path from the wireframe JSON path."""
|
||||
d = os.path.dirname(wireframe_path)
|
||||
base = os.path.splitext(os.path.basename(wireframe_path))[0]
|
||||
return os.path.join(d, f".{base}.idmap.json")
|
||||
|
||||
|
||||
def load_mapping(wireframe_path):
|
||||
p = mapping_path(wireframe_path)
|
||||
if os.path.exists(p):
|
||||
with open(p) as f:
|
||||
return json.load(f)
|
||||
return {"page_id": None, "shapes": {}, "connectors": {}}
|
||||
|
||||
|
||||
def save_mapping(wireframe_path, mapping):
|
||||
p = mapping_path(wireframe_path)
|
||||
os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
|
||||
with open(p, "w") as f:
|
||||
json.dump(mapping, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
# -- Push: local JSON -> Frame0 -----------------------------------------------
|
||||
|
||||
# Frame0 returns different type names from get vs what create accepts.
|
||||
# Map internal types back to create-API types.
|
||||
TYPE_TO_CREATE = {
|
||||
"Box": "Rectangle",
|
||||
"box": "Rectangle",
|
||||
}
|
||||
|
||||
|
||||
def topo_sort_shapes(shapes):
|
||||
"""Sort shape IDs so parents come before children."""
|
||||
order = []
|
||||
visited = set()
|
||||
|
||||
def visit(sid):
|
||||
if sid in visited:
|
||||
return
|
||||
visited.add(sid)
|
||||
shape = shapes[sid]
|
||||
parent = shape.get("parent")
|
||||
if parent and parent in shapes:
|
||||
visit(parent)
|
||||
order.append(sid)
|
||||
|
||||
for sid in shapes:
|
||||
visit(sid)
|
||||
return order
|
||||
|
||||
|
||||
def find_or_create_page(port, name, mapping):
|
||||
"""Find existing page by mapping or name, or create a new one."""
|
||||
# Try mapped page_id first
|
||||
if mapping.get("page_id"):
|
||||
try:
|
||||
page = api(port, "page:get", {"pageId": mapping["page_id"]})
|
||||
if page:
|
||||
return mapping["page_id"]
|
||||
except SystemExit:
|
||||
pass # Page no longer exists, fall through
|
||||
|
||||
# Search by name in existing pages
|
||||
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||
if doc and "children" in doc:
|
||||
for page in doc["children"]:
|
||||
if page.get("name") == name:
|
||||
return page["id"]
|
||||
|
||||
# Create new page
|
||||
page = api(port, "page:add", {"pageProps": {"name": name}})
|
||||
return page["id"]
|
||||
|
||||
|
||||
def clear_page(port, page_id):
|
||||
"""Delete all shapes on a page."""
|
||||
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||
if not page or "children" not in page:
|
||||
return
|
||||
shape_ids = [s["id"] for s in page["children"]]
|
||||
if shape_ids:
|
||||
api(port, "edit:delete", {"shapeIdArray": shape_ids})
|
||||
|
||||
|
||||
def push(wireframe_path, port):
|
||||
"""Push local wireframe JSON to Frame0."""
|
||||
with open(wireframe_path) as f:
|
||||
wireframe = json.load(f)
|
||||
|
||||
name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0])
|
||||
shapes = wireframe.get("shapes", {})
|
||||
connectors = wireframe.get("connectors", {})
|
||||
|
||||
mapping = load_mapping(wireframe_path)
|
||||
|
||||
# Find or create page
|
||||
page_id = find_or_create_page(port, name, mapping)
|
||||
mapping["page_id"] = page_id
|
||||
|
||||
# Switch to page and clear it
|
||||
api(port, "page:set-current-page", {"pageId": page_id})
|
||||
clear_page(port, page_id)
|
||||
|
||||
# Reset ID mappings (shapes are recreated)
|
||||
mapping["shapes"] = {}
|
||||
mapping["connectors"] = {}
|
||||
|
||||
# Create shapes in dependency order
|
||||
order = topo_sort_shapes(shapes)
|
||||
for local_id in order:
|
||||
shape = shapes[local_id]
|
||||
raw_type = shape.get("type", "Rectangle")
|
||||
shape_type = TYPE_TO_CREATE.get(raw_type, raw_type)
|
||||
parent_local = shape.get("parent")
|
||||
|
||||
# Build shapeProps from everything except meta fields
|
||||
meta_keys = {"type", "parent"}
|
||||
props = {k: v for k, v in shape.items() if k not in meta_keys}
|
||||
|
||||
# Set name to local_id if not explicitly set
|
||||
if "name" not in props:
|
||||
props["name"] = local_id
|
||||
|
||||
create_args = {
|
||||
"type": shape_type,
|
||||
"shapeProps": props,
|
||||
"convertColors": True,
|
||||
}
|
||||
|
||||
# Resolve parent ID
|
||||
if parent_local and parent_local in mapping["shapes"]:
|
||||
create_args["parentId"] = mapping["shapes"][parent_local]
|
||||
|
||||
f0_id = api(port, "shape:create-shape", create_args)
|
||||
mapping["shapes"][local_id] = f0_id
|
||||
|
||||
# Create connectors
|
||||
for local_id, conn in connectors.items():
|
||||
tail_local = conn.get("tailId")
|
||||
head_local = conn.get("headId")
|
||||
|
||||
if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]:
|
||||
print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr)
|
||||
continue
|
||||
|
||||
meta_keys = {"tailId", "headId"}
|
||||
props = {k: v for k, v in conn.items() if k not in meta_keys}
|
||||
if "name" not in props:
|
||||
props["name"] = local_id
|
||||
|
||||
f0_id = api(port, "shape:create-connector", {
|
||||
"tailId": mapping["shapes"][tail_local],
|
||||
"headId": mapping["shapes"][head_local],
|
||||
"shapeProps": props,
|
||||
"convertColors": True,
|
||||
})
|
||||
mapping["connectors"][local_id] = f0_id
|
||||
|
||||
# Fit to screen
|
||||
api(port, "view:fit-to-screen")
|
||||
|
||||
save_mapping(wireframe_path, mapping)
|
||||
total = len(mapping["shapes"]) + len(mapping["connectors"])
|
||||
print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors")
|
||||
|
||||
|
||||
# -- Pull: Frame0 -> local JSON -----------------------------------------------
|
||||
|
||||
def pull(page_ref, output_path, port):
|
||||
"""Pull a Frame0 page into local wireframe JSON."""
|
||||
# Resolve page_ref: could be an ID or a name
|
||||
page_id = None
|
||||
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||
if doc and "children" in doc:
|
||||
for page in doc["children"]:
|
||||
if page["id"] == page_ref or page.get("name") == page_ref:
|
||||
page_id = page["id"]
|
||||
page_name = page.get("name", page_ref)
|
||||
break
|
||||
|
||||
if not page_id:
|
||||
print(f"ERROR: Page not found: {page_ref}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Load existing mapping for reverse lookup
|
||||
mapping = load_mapping(output_path)
|
||||
reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()}
|
||||
reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()}
|
||||
|
||||
# Get full page with shapes
|
||||
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||
|
||||
shapes = {}
|
||||
connectors = {}
|
||||
new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}}
|
||||
auto_id_counter = [0]
|
||||
|
||||
def auto_id(f0_shape):
|
||||
"""Generate a stable local ID from shape name or auto-number."""
|
||||
# Prefer existing mapping
|
||||
f0_id = f0_shape["id"]
|
||||
if f0_id in reverse_map:
|
||||
return reverse_map[f0_id]
|
||||
# Use sanitized name
|
||||
name = f0_shape.get("name", "")
|
||||
if name:
|
||||
sanitized = name.lower().replace(" ", "-").replace("_", "-")
|
||||
if sanitized not in shapes:
|
||||
return sanitized
|
||||
# Fallback: auto-number
|
||||
auto_id_counter[0] += 1
|
||||
return f"s{auto_id_counter[0]:03d}"
|
||||
|
||||
def process_shape(f0_shape, parent_local_id=None):
|
||||
f0_id = f0_shape["id"]
|
||||
local_id = auto_id(f0_shape)
|
||||
new_mapping["shapes"][local_id] = f0_id
|
||||
|
||||
# Extract shape properties — only strip structural keys that our
|
||||
# ID mapping replaces. Everything else passes through as-is so the
|
||||
# local JSON speaks Frame0's native vocabulary.
|
||||
shape_type = f0_shape.get("type", "Box")
|
||||
skip_keys = {"id", "type", "children", "pageId", "parentId"}
|
||||
props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None}
|
||||
|
||||
entry = {"type": shape_type}
|
||||
if parent_local_id:
|
||||
entry["parent"] = parent_local_id
|
||||
entry.update(props)
|
||||
|
||||
# Remove name if it matches local_id (redundant)
|
||||
if entry.get("name") == local_id:
|
||||
del entry["name"]
|
||||
|
||||
shapes[local_id] = entry
|
||||
|
||||
# Process children recursively
|
||||
for child in f0_shape.get("children", []):
|
||||
child_type = child.get("type", "")
|
||||
if child_type == "Connector":
|
||||
process_connector(child)
|
||||
else:
|
||||
process_shape(child, local_id)
|
||||
|
||||
def process_connector(f0_conn):
|
||||
f0_id = f0_conn["id"]
|
||||
local_id = reverse_conn.get(f0_id)
|
||||
if not local_id:
|
||||
auto_id_counter[0] += 1
|
||||
local_id = f"c{auto_id_counter[0]:03d}"
|
||||
|
||||
new_mapping["connectors"][local_id] = f0_id
|
||||
|
||||
tail_f0 = f0_conn.get("tail", {}).get("id")
|
||||
head_f0 = f0_conn.get("head", {}).get("id")
|
||||
|
||||
entry = {}
|
||||
if tail_f0:
|
||||
# Will be resolved after all shapes are processed
|
||||
entry["_tailF0"] = tail_f0
|
||||
if head_f0:
|
||||
entry["_headF0"] = head_f0
|
||||
|
||||
skip_keys = {"id", "type", "children", "pageId", "tail", "head"}
|
||||
props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None}
|
||||
entry.update(props)
|
||||
|
||||
connectors[local_id] = entry
|
||||
|
||||
# Process all top-level shapes
|
||||
for child in page.get("children", []):
|
||||
child_type = child.get("type", "")
|
||||
if child_type == "Connector":
|
||||
process_connector(child)
|
||||
else:
|
||||
process_shape(child)
|
||||
|
||||
# Resolve connector references to local IDs
|
||||
f0_to_local = {v: k for k, v in new_mapping["shapes"].items()}
|
||||
for conn in connectors.values():
|
||||
tail_f0 = conn.pop("_tailF0", None)
|
||||
head_f0 = conn.pop("_headF0", None)
|
||||
if tail_f0 and tail_f0 in f0_to_local:
|
||||
conn["tailId"] = f0_to_local[tail_f0]
|
||||
if head_f0 and head_f0 in f0_to_local:
|
||||
conn["headId"] = f0_to_local[head_f0]
|
||||
|
||||
wireframe = {"name": page_name}
|
||||
if shapes:
|
||||
wireframe["shapes"] = shapes
|
||||
if connectors:
|
||||
wireframe["connectors"] = connectors
|
||||
|
||||
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(wireframe, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
save_mapping(output_path, new_mapping)
|
||||
print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors")
|
||||
|
||||
|
||||
# -- Export: push then export as image -----------------------------------------
|
||||
|
||||
def export_image(wireframe_path, output_path, port, fmt="image/png"):
|
||||
"""Push wireframe to Frame0 and export the page as an image."""
|
||||
import base64
|
||||
|
||||
# Push first to ensure Frame0 is up to date
|
||||
push(wireframe_path, port)
|
||||
|
||||
mapping = load_mapping(wireframe_path)
|
||||
page_id = mapping.get("page_id")
|
||||
if not page_id:
|
||||
print("ERROR: No page_id in mapping after push", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
image_b64 = api(port, "file:export-image", {
|
||||
"pageId": page_id,
|
||||
"format": fmt,
|
||||
"fillBackground": True,
|
||||
})
|
||||
|
||||
image_bytes = base64.b64decode(image_b64)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)")
|
||||
|
||||
|
||||
# -- CLI -----------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0")
|
||||
parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT)))
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
|
||||
p_push = sub.add_parser("push", help="Push local JSON to Frame0")
|
||||
p_push.add_argument("wireframe", help="Path to wireframe .json file")
|
||||
|
||||
p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON")
|
||||
p_pull.add_argument("page", help="Page ID or page name")
|
||||
p_pull.add_argument("output", help="Output .json path")
|
||||
|
||||
p_export = sub.add_parser("export", help="Push and export as image")
|
||||
p_export.add_argument("wireframe", help="Path to wireframe .json file")
|
||||
p_export.add_argument("output", help="Output image path (e.g. wireframe.png)")
|
||||
p_export.add_argument("--format", default="image/png",
|
||||
help="Export MIME type (default: image/png)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "push":
|
||||
push(args.wireframe, args.port)
|
||||
elif args.command == "pull":
|
||||
pull(args.page, args.output, args.port)
|
||||
elif args.command == "export":
|
||||
export_image(args.wireframe, args.output, args.port, args.format)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,209 +0,0 @@
|
||||
---
|
||||
name: theme-ui
|
||||
description: >-
|
||||
Token selection guide for clide UI development. Use when building or
|
||||
modifying widgets, panels, pane chrome, status indicators, icons, or
|
||||
any visual surface. Ensures correct background, border, text, and hover
|
||||
tokens are applied per surface type. Triggers on: new widget code,
|
||||
theme-related changes, "which token", "what color", color/background
|
||||
questions, visual inconsistency fixes, new panel/pane/view development,
|
||||
adding or looking up Phosphor icons, icon codepoints.
|
||||
---
|
||||
|
||||
# Theme-UI — token selection for clide surfaces
|
||||
|
||||
All colors come from `SurfaceTokens` via `ClideTheme.of(context).surface`.
|
||||
Never hardcode colors. Never use Material/Cupertino color constants.
|
||||
|
||||
## Token selection by surface
|
||||
|
||||
Pick tokens based on **where** the widget lives, not what it does.
|
||||
|
||||
### Chrome (hat bar, status bar, sidebar, context panel, spines, drag handles)
|
||||
|
||||
```
|
||||
background → chromeBackground
|
||||
text → chromeForeground
|
||||
border → chromeBorder (1px)
|
||||
active text → globalForeground
|
||||
```
|
||||
|
||||
### Side panels (sidebar, context panel)
|
||||
|
||||
```
|
||||
background → chromeBackground (both sides — they're chrome frame)
|
||||
text → sidebarForeground
|
||||
hover → sidebarItemHover
|
||||
selected → sidebarItemSelected
|
||||
section head → sidebarSectionHeader (muted, used for "START", "FILES", etc.)
|
||||
```
|
||||
|
||||
Padding: 2px on outer edges, 0px on divider edge.
|
||||
|
||||
### Center column (workspace, Claude pane, editor)
|
||||
|
||||
```
|
||||
background → panelBackground
|
||||
text → globalForeground
|
||||
```
|
||||
|
||||
No padding — content fills edge to edge.
|
||||
|
||||
### Pane headers (`ClidePaneChrome`)
|
||||
|
||||
```
|
||||
background → panelHeader
|
||||
text (title) → panelHeaderForeground
|
||||
text (sub) → globalTextMuted
|
||||
```
|
||||
|
||||
### List items (decisions, tickets, file rows, backlinks)
|
||||
|
||||
```
|
||||
background → (none / transparent)
|
||||
hover bg → listItemHoverBackground
|
||||
selected bg → listItemSelectedBackground
|
||||
text → listItemForeground / sidebarForeground (in sidebar)
|
||||
selected txt → listItemSelectedForeground
|
||||
```
|
||||
|
||||
In sidebar context, use `sidebarItemHover` not `listItemHoverBackground`.
|
||||
|
||||
### Buttons
|
||||
|
||||
```
|
||||
normal → buttonBackground / buttonForeground / buttonBorder
|
||||
hover → buttonHoverBackground
|
||||
active → buttonActiveBackground
|
||||
primary → buttonActiveBackground bg + globalBackground text
|
||||
subtle → listItemBackground / listItemHoverBackground (no border)
|
||||
```
|
||||
|
||||
### Dividers and separators
|
||||
|
||||
```
|
||||
line → dividerColor (always, everywhere)
|
||||
drag handle → 8px hit area, 1px visible line, panel bg fill
|
||||
hover line → panelActiveBorder
|
||||
```
|
||||
|
||||
### Status indicators
|
||||
|
||||
```
|
||||
success/ok → statusSuccess (green: done, added, connected)
|
||||
warning → statusWarning (amber: question, modified, missing)
|
||||
error → statusError (red: deleted, rejected, cancelled)
|
||||
info → statusInfo (blue: in_progress, modified)
|
||||
```
|
||||
|
||||
Map semantic states, not visual styles:
|
||||
- `done` / `added` / `ok` → `statusSuccess`
|
||||
- `in_progress` / `modified` → `statusInfo`
|
||||
- `question` / `warning` → `statusWarning`
|
||||
- `cancelled` / `deleted` / `error` → `statusError`
|
||||
|
||||
### Overlays (dialogs, palette, tooltips)
|
||||
|
||||
```
|
||||
dialog bg → modalSurfaceBackground
|
||||
dialog border→ modalSurfaceBorder
|
||||
backdrop → modalOverlayBackground
|
||||
tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
||||
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||
```
|
||||
|
||||
## Typography
|
||||
|
||||
Three constants — never hardcode sizes or families.
|
||||
|
||||
```
|
||||
family UI → inherited from DefaultTextStyle (JosefinSans Light 300)
|
||||
family mono → clideMonoFamily (JetBrainsMono)
|
||||
body size → clideFontBody (15)
|
||||
caption size → clideFontCaption (14) — status bar, section headers, git info
|
||||
mono size → clideFontMono (14) — terminal, code, paths, IDs
|
||||
```
|
||||
|
||||
Use `ClideText` for themed text. Set `muted: true` for secondary text
|
||||
(resolves to `globalTextMuted`). Set `fontFamily: clideMonoFamily` for
|
||||
code/paths/IDs. Don't set fontFamily for UI text — it inherits.
|
||||
|
||||
## Token identity rule
|
||||
|
||||
Every visual surface gets its own named token. Never borrow a token from
|
||||
another surface just because they happen to resolve to the same color.
|
||||
|
||||
**Wrong:** `sidebarBackground` for the hat bar (the hat isn't a sidebar).
|
||||
**Right:** Create `chromeBackground` that resolves to the same palette key.
|
||||
|
||||
When two surfaces share a color:
|
||||
1. **If they're the same conceptual surface** (sidebar + context panel are
|
||||
both "side panels") → one shared token set is fine.
|
||||
2. **If they're different surfaces that happen to match** (hat bar + sidebar
|
||||
+ status bar are all "chrome frame") → create a shared primitive in the
|
||||
palette/semantic layer (e.g. `bgChrome`) and give each surface its own
|
||||
token that maps to that primitive. This lets themes diverge them later.
|
||||
|
||||
The palette layer has these depth primitives:
|
||||
- `bg` (`#20202C`) — outermost root, behind everything
|
||||
- `bgSunken` (`#1A1A24`) — chrome frame: sidebar, hat, statusbar
|
||||
- `surface` (`#242838`) — elevated: pane headers, active tabs
|
||||
- `surfaceHi` (`#2C3046`) — interactive: hover states, selections
|
||||
|
||||
Chrome tokens (`chromeBackground`/`chromeForeground`/`chromeBorder`) are
|
||||
the shared root for all frame surfaces. They resolve to `bgSunken` /
|
||||
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||
hat from sidebar from status bar if desired.
|
||||
|
||||
## Extension-owned domain colors
|
||||
|
||||
Extensions that need domain-specific color coding (ticket types, decision
|
||||
types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead:
|
||||
|
||||
1. Create a color map class in the extension (e.g. `TicketTypeColors`)
|
||||
2. Ship dark and light presets, auto-selected via `ClideTheme.of(context).dark`
|
||||
3. Store user overrides under `ext.<id>.colors` in settings
|
||||
4. Reference: `lib/builtin/tickets/src/ticket_colors.dart`
|
||||
|
||||
This keeps the core token surface lean and lets each extension own its
|
||||
palette. The pattern scales to any extension needing domain colors.
|
||||
|
||||
## Icons — Phosphor Icons
|
||||
|
||||
The app bundles Phosphor Icons (v2.0.8, MIT) as TTF fonts at
|
||||
`assets/fonts/phosphor/` (regular, bold, fill weights).
|
||||
|
||||
**Codepoint reference:** `assets/fonts/phosphor/codepoints.csv` —
|
||||
full mapping of all 1512 icon codepoints to kebab-case and PascalCase
|
||||
names. Read this file to look up any icon by name or codepoint.
|
||||
|
||||
**Adding an icon:** find the codepoint in `codepoints.csv`, then add
|
||||
a `static const` entry to `PhosphorIcons` in
|
||||
`lib/widgets/src/icons/phosphor.dart`:
|
||||
|
||||
```dart
|
||||
static const arrowClockwise = PhosphorIconPainter(0xe036);
|
||||
```
|
||||
|
||||
Only add icons we actually use — don't bulk-import the full set.
|
||||
|
||||
**Using an icon:** `ClideIcon(PhosphorIcons.arrowClockwise, size: 13)`
|
||||
or as a `TabContribution` icon field: `icon: PhosphorIcons.lightbulb`.
|
||||
|
||||
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||
Fill weight: `family: 'Phosphor-Fill'`.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Borrowing another surface's token (`sidebarBackground` for hat bar)
|
||||
- `globalBackground` for panel fill → use `panelBackground`
|
||||
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||
- Hardcoded `Color(0xFF...)` → use a token
|
||||
- `fontSize: 14` → use `clideFontCaption` or `clideFontMono`
|
||||
- `fontFamily: 'JetBrainsMono'` → use `clideMonoFamily`
|
||||
|
||||
## Reference
|
||||
|
||||
Full token list: `lib/kernel/src/theme/tokens.dart`
|
||||
Resolver fallbacks: `lib/kernel/src/theme/resolver.dart`
|
||||
Theme YAML example: `lib/kernel/src/theme/themes/clide.yaml`
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: ui-design
|
||||
description: >-
|
||||
Visual design guide for clide UI development — covers theme tokens,
|
||||
surface-specific token selection, control geometry/spacing/alignment,
|
||||
and Phosphor icons. Use when building or modifying widgets, panels,
|
||||
pane chrome, status indicators, tabs, list items, dialogs, or any
|
||||
visual surface. Triggers on: new widget code, theme-related changes,
|
||||
"which token", "what color", color/background questions, visual
|
||||
inconsistency fixes, "alignment off", "spacing", "padding", control
|
||||
geometry questions, new panel/pane/view development, adding or
|
||||
looking up Phosphor icons, icon codepoints.
|
||||
---
|
||||
|
||||
# UI design — clide visual surface guide
|
||||
|
||||
This skill bundles four concerns that all surface in widget work:
|
||||
|
||||
| Concern | Reference | When to read |
|
||||
|---------|-----------|--------------|
|
||||
| Token system, palette, typography | [`references/theme.md`](references/theme.md) | Designing or extending the theme pipeline; deciding whether to add a new token |
|
||||
| Token selection per surface | [`references/surface.md`](references/surface.md) | Building a new widget or modifying an existing one — "which token does this need" |
|
||||
| Spacing, alignment, control layout | [`references/geometry.md`](references/geometry.md) | Building tab strips, list items, buttons, anything where icons sit next to text or padded edges |
|
||||
| Phosphor icon usage and codepoints | [`references/icons.md`](references/icons.md) | Adding or referencing an icon |
|
||||
|
||||
Read the reference that matches the question. They cross-reference each
|
||||
other where relevant; you don't need to read all four.
|
||||
|
||||
## Universal rules
|
||||
|
||||
These apply across every reference and every surface:
|
||||
|
||||
- All colors come from `SurfaceTokens` via `ClideTheme.of(context).surface`.
|
||||
Never hardcode `Color(0xFF...)`.
|
||||
- Never use `Material*` or `Cupertino*` widgets or color constants — clide
|
||||
is `WidgetsApp` only (D-7).
|
||||
- Use `ClideText` for themed text; never bare `Text` in production widgets.
|
||||
- Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for
|
||||
status/section headers, body inherits from `DefaultTextStyle`.
|
||||
|
||||
## Anti-patterns (cross-cutting)
|
||||
|
||||
- Borrowing another surface's token (`sidebarBackground` for hat bar) — give
|
||||
each surface its own token even if they share a palette key. See `theme.md`.
|
||||
- Hardcoded hex colors → use a token. See `surface.md` for which one.
|
||||
- `fontSize: 14` literal → use `clideFontCaption` or `clideFontMono`.
|
||||
- `fontFamily: 'JetBrainsMono'` literal → use `clideMonoFamily`.
|
||||
- Stacking edge padding on a padded parent + a padded child action → see
|
||||
`geometry.md` "no double edge padding".
|
||||
- Eyeballing pixel margins instead of working back from the constraint —
|
||||
the math matters; see `geometry.md` "uniform inner spacing".
|
||||
@@ -0,0 +1,181 @@
|
||||
# Geometry — spacing, alignment, control layout
|
||||
|
||||
Principles for placing icons, buttons, and text inside controls.
|
||||
Apply when building tab strips, list items, buttons with affordances,
|
||||
or anything where actions sit next to content.
|
||||
|
||||
> Constants live in `lib/widgets/src/spacing.dart` — pull from there
|
||||
> instead of inlining literals:
|
||||
>
|
||||
> | Concept | Constant |
|
||||
> |-------------------------|-------------------------|
|
||||
> | Hairline (2px) | `clideInsetHairline` |
|
||||
> | Tight inset (4px) | `clideInsetTight` |
|
||||
> | Uniform icon margin (6) | `clideInsetIcon` |
|
||||
> | Standard inset (8px) | `clideInsetStandard` |
|
||||
> | Text-content inset (12) | `clideInsetText` |
|
||||
> | Tight / standard gap | `clideGapTight` / `clideGapStandard` |
|
||||
> | Section / major gap | `clideGapSection` / `clideGapMajor` |
|
||||
> | Micro icon (10) | `clideIconMicro` |
|
||||
> | Standard icon (14) | `clideIconStandard` |
|
||||
> | Hit-target (16) | `clideIconHitTarget` |
|
||||
> | Control height (28) | `clideControlHeight` |
|
||||
|
||||
## Uniform inner spacing rule
|
||||
|
||||
Icons inside control surfaces should have **equal margin on every
|
||||
constrained side**. The "constrained sides" are top, bottom, and the
|
||||
side opposite to where content flows in.
|
||||
|
||||
The remaining side — where the text or other content sits — gets a
|
||||
larger, content-appropriate breathing room.
|
||||
|
||||
Example: tab close button (16×16 inside a 28-tall tab):
|
||||
|
||||
```
|
||||
top : 6 ┐
|
||||
bottom : 6 ├─ uniform: (28 − 16) / 2 = 6
|
||||
right : 6 ┘
|
||||
left : 8 ── content gap (separates from title text)
|
||||
```
|
||||
|
||||
The visual effect: the close button looks like a deliberate
|
||||
affordance with a calm, consistent border, not a glyph stuffed into
|
||||
the corner.
|
||||
|
||||
## No double-edge padding
|
||||
|
||||
When a fixed-size action (icon button, close ×) sits at the edge of
|
||||
a padded parent, the parent's padding on that edge should **not stack**
|
||||
with the action's own internal margin. Pick one place to hold the
|
||||
breathing room.
|
||||
|
||||
Wrong:
|
||||
|
||||
```dart
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12), // tab pad: 12 right
|
||||
child: Row(children: [
|
||||
Expanded(child: title),
|
||||
SizedBox(width: 8), // gap: 8
|
||||
Container(width: 28, alignment: Center, // close: 6 internal margin
|
||||
child: Icon(close, size: 16)),
|
||||
]),
|
||||
)
|
||||
// Visible margin from icon right to outer right = 12 + 6 = 18px → too much
|
||||
```
|
||||
|
||||
Right:
|
||||
|
||||
```dart
|
||||
Container(
|
||||
padding: EdgeInsets.only(left: 12, right: 6), // pad matches icon margin
|
||||
child: Row(children: [
|
||||
Expanded(child: title),
|
||||
SizedBox(width: 8),
|
||||
Container(width: 16, height: 16, alignment: Center, // hit target = icon size
|
||||
child: Icon(close, size: 10)),
|
||||
]),
|
||||
)
|
||||
// Visible margin = 6 (parent right pad) ≈ 6 (top/bottom auto) → uniform
|
||||
```
|
||||
|
||||
## Two-column control pattern
|
||||
|
||||
For tab-shaped or row-shaped controls with a primary content area and
|
||||
a secondary action:
|
||||
|
||||
```dart
|
||||
Row(children: [
|
||||
Expanded(child: <content>), // takes remainder
|
||||
if (action != null) ...[
|
||||
SizedBox(width: 8), // standard gap
|
||||
<fixed-size action>, // shrinks to content
|
||||
],
|
||||
])
|
||||
```
|
||||
|
||||
- **Left column**: `Expanded`, holds the primary content (title,
|
||||
label, description). Aligned to the start of its space by default.
|
||||
- **Right column**: fixed natural width, holds the action (close,
|
||||
status, indicator). Sized to the icon, not to artificial padding.
|
||||
|
||||
The parent container's padding sits flush against both columns (see
|
||||
"no double-edge padding").
|
||||
|
||||
## Match perceived mass, not measured pixels
|
||||
|
||||
Glyphs vary in visual weight. A bold `+` looks heavier than a thin
|
||||
`×` at the same point size. When eyeballing alignment, trust the
|
||||
optical center over the geometric center.
|
||||
|
||||
In practice: if two icons measure to the same margin but one *looks*
|
||||
crowded, give the heavier glyph slightly more breathing room and
|
||||
trim the lighter one. For clide, this came up with the `×` close
|
||||
glyph vs the `+` add glyph — both at 14pt, but `+` reads as denser
|
||||
and is left in its 28-wide button without further padding, while
|
||||
`×` sits in a 16×16 hit area with 6px symmetric margin.
|
||||
|
||||
## Strip / row should fill the parent
|
||||
|
||||
Tab strips, status bars, and divider rows should span the full
|
||||
parent width, not size to their content. Without this, the strip
|
||||
looks like it floats inside the pane.
|
||||
|
||||
```dart
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // <-- this
|
||||
children: [
|
||||
_TabStrip(...),
|
||||
Expanded(child: _body(...)),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Without `stretch`, Column gives loose width constraints and a
|
||||
`Container(height: tabHeight)` child sizes to its child's natural
|
||||
width — the strip ends mid-pane.
|
||||
|
||||
## Anchor strips with a divider
|
||||
|
||||
Add a 1px bottom border (`dividerColor`) to tab strips and any
|
||||
header strip that sits above content. Without it, the strip looks
|
||||
disconnected from the body and the perceived alignment slips.
|
||||
|
||||
```dart
|
||||
Container(
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.tabBarBackground,
|
||||
border: Border(bottom: BorderSide(color: tokens.dividerColor)),
|
||||
),
|
||||
child: ...,
|
||||
)
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Centering a glyph inside a "hover background" that's larger than
|
||||
the natural icon size, then surrounding the whole thing with a
|
||||
padded parent — the icon ends up far inside the visible edge.
|
||||
- Hardcoded `padding: EdgeInsets.symmetric(horizontal: 12)` on every
|
||||
control regardless of whether the right edge has an action — see
|
||||
"no double-edge padding".
|
||||
- Tab strip inside `Column` without `crossAxisAlignment.stretch` —
|
||||
the strip ends mid-pane.
|
||||
- `mainAxisSize.min` on the tab strip's outer Row when you actually
|
||||
want it to fill parent width — only use `min` for pill-shaped
|
||||
controls that should hug their content.
|
||||
- Eyeballing alignment without working back from a target margin in
|
||||
pixels. The math matters; see "uniform inner spacing".
|
||||
|
||||
## Testing alignment
|
||||
|
||||
When iterating on a control's spacing:
|
||||
|
||||
1. State the target margin (e.g. "6px around the close icon, all
|
||||
sides except left").
|
||||
2. Map every contributing source: parent padding, gap SizedBoxes,
|
||||
container alignment offsets, icon-to-container size differences.
|
||||
3. Sum them. Adjust until they hit the target.
|
||||
4. Verify visually — perceived mass may justify a 1–2px tweak.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Icons — Phosphor + clide-owned painters
|
||||
|
||||
## Phosphor Icons
|
||||
|
||||
The app bundles Phosphor Icons (v2.0.8, MIT) as TTF fonts at
|
||||
`assets/fonts/phosphor/` (regular, bold, fill weights).
|
||||
|
||||
**Codepoint reference:** `assets/fonts/phosphor/codepoints.csv` — full
|
||||
mapping of all 1512 icon codepoints to kebab-case and PascalCase
|
||||
names. Read this file to look up any icon by name or codepoint.
|
||||
|
||||
### Adding an icon
|
||||
|
||||
Find the codepoint in `codepoints.csv`, then add a `static const`
|
||||
entry to `PhosphorIcons` in `lib/widgets/src/icons/phosphor.dart`:
|
||||
|
||||
```dart
|
||||
static const arrowClockwise = PhosphorIconPainter(0xe036);
|
||||
```
|
||||
|
||||
Only add icons we actually use — don't bulk-import the full set.
|
||||
|
||||
### Using an icon
|
||||
|
||||
```dart
|
||||
ClideIcon(PhosphorIcons.arrowClockwise, size: 13)
|
||||
```
|
||||
|
||||
Or as a `TabContribution` icon field: `icon: PhosphorIcons.lightbulb`.
|
||||
|
||||
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||
**Fill weight:** `family: 'Phosphor-Fill'`.
|
||||
|
||||
## clide-owned painters
|
||||
|
||||
Some shapes are simple enough to paint directly without an icon
|
||||
font. Hand-rolled `ClideIconPainter` subclasses live under
|
||||
`lib/widgets/src/icons/`:
|
||||
|
||||
- `CheckIcon`, `ChevronIcon`, `CloseIcon` (`x.dart`)
|
||||
- `DotIcon`, `FolderIcon`, `GearIcon`
|
||||
- `GitBranchIcon`, `PlugIcon`, `SearchIcon`
|
||||
- `TerminalIcon`, `WarningIcon`
|
||||
|
||||
Use these for tiny, theme-aware glyphs (close ×, dropdown chevrons,
|
||||
status dots) where pulling in the Phosphor font weight would be
|
||||
overkill or where the visual needs to match the theme's stroke
|
||||
weight conventions.
|
||||
|
||||
Pattern for a new painter:
|
||||
|
||||
```dart
|
||||
class FoobarIcon extends ClideIconPainter {
|
||||
const FoobarIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
// Coordinates are 0..1 (the painter is given a unit square).
|
||||
canvas.drawLine(const Offset(0.2, 0.2), const Offset(0.8, 0.8), p);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sizing
|
||||
|
||||
Icon sizes used in clide (subject to consolidation under
|
||||
`ClideSpacing` — see T-86):
|
||||
|
||||
- `10` — micro: close × inside a tab
|
||||
- `13` — caption-row icons (sidebar, status bar)
|
||||
- `14` — standard inline icons (icon rail)
|
||||
- `16` — small icon hit-target outer container
|
||||
- `18`–`20` — emphatic / standalone icons
|
||||
|
||||
Pass `size:` to `ClideIcon`; the painter receives a unit-square
|
||||
canvas regardless. Color defaults to `globalForeground`; pass
|
||||
explicit `color:` for muted/active variants.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Importing all of Phosphor — only declare codepoints we use.
|
||||
- Hand-painting a glyph that already exists in Phosphor at the right
|
||||
weight — use the font.
|
||||
- Hardcoded `Color` on icons — pass through the surface tokens
|
||||
(`globalForeground`, `globalTextMuted`, `panelActiveBorder`, etc.).
|
||||
@@ -0,0 +1,120 @@
|
||||
# Surface — token selection per surface type
|
||||
|
||||
Pick tokens based on **where** the widget lives, not what it does.
|
||||
|
||||
## Chrome (hat bar, status bar, sidebar, context panel, spines, drag handles)
|
||||
|
||||
```
|
||||
background → chromeBackground
|
||||
text → chromeForeground
|
||||
border → chromeBorder (1px)
|
||||
active text → globalForeground
|
||||
```
|
||||
|
||||
## Side panels (sidebar, context panel)
|
||||
|
||||
```
|
||||
background → chromeBackground (both sides — they're chrome frame)
|
||||
text → sidebarForeground
|
||||
hover → sidebarItemHover
|
||||
selected → sidebarItemSelected
|
||||
section head → sidebarSectionHeader (muted, used for "START", "FILES", etc.)
|
||||
```
|
||||
|
||||
Padding: 2px on outer edges, 0px on divider edge.
|
||||
|
||||
## Center column (workspace, Claude pane, editor)
|
||||
|
||||
```
|
||||
background → panelBackground
|
||||
text → globalForeground
|
||||
```
|
||||
|
||||
No padding — content fills edge to edge.
|
||||
|
||||
## Pane headers (`ClidePaneChrome`)
|
||||
|
||||
```
|
||||
background → panelHeader
|
||||
text (title) → panelHeaderForeground
|
||||
text (sub) → globalTextMuted
|
||||
```
|
||||
|
||||
## Tabs (`MultitabPane`, `ClideTabBar`)
|
||||
|
||||
```
|
||||
strip bg → tabBarBackground
|
||||
strip border → bottom: dividerColor (anchors strip to body)
|
||||
active fg → tabActiveForeground
|
||||
inactive fg → tabInactiveForeground
|
||||
active bg → panelHeader (elevated chrome)
|
||||
inactive bg → tabBarBackground (blends with strip)
|
||||
active border→ panelActiveBorder (top accent, 1.5px)
|
||||
side border → panelBorder
|
||||
```
|
||||
|
||||
For control geometry inside tabs (close button placement, padding,
|
||||
two-column title+action layout) see [`geometry.md`](geometry.md).
|
||||
|
||||
## List items (decisions, tickets, file rows, backlinks)
|
||||
|
||||
```
|
||||
background → (none / transparent)
|
||||
hover bg → listItemHoverBackground
|
||||
selected bg → listItemSelectedBackground
|
||||
text → listItemForeground / sidebarForeground (in sidebar)
|
||||
selected txt → listItemSelectedForeground
|
||||
```
|
||||
|
||||
In sidebar context, use `sidebarItemHover` not `listItemHoverBackground`.
|
||||
|
||||
## Buttons
|
||||
|
||||
```
|
||||
normal → buttonBackground / buttonForeground / buttonBorder
|
||||
hover → buttonHoverBackground
|
||||
active → buttonActiveBackground
|
||||
primary → buttonActiveBackground bg + globalBackground text
|
||||
subtle → listItemBackground / listItemHoverBackground (no border)
|
||||
```
|
||||
|
||||
## Dividers and separators
|
||||
|
||||
```
|
||||
line → dividerColor (always, everywhere)
|
||||
drag handle → 8px hit area, 1px visible line, panel bg fill
|
||||
hover line → panelActiveBorder
|
||||
```
|
||||
|
||||
## Status indicators
|
||||
|
||||
```
|
||||
success/ok → statusSuccess (green: done, added, connected)
|
||||
warning → statusWarning (amber: question, modified, missing)
|
||||
error → statusError (red: deleted, rejected, cancelled)
|
||||
info → statusInfo (blue: in_progress, modified)
|
||||
```
|
||||
|
||||
Map semantic states, not visual styles:
|
||||
|
||||
- `done` / `added` / `ok` → `statusSuccess`
|
||||
- `in_progress` / `modified` → `statusInfo`
|
||||
- `question` / `warning` → `statusWarning`
|
||||
- `cancelled` / `deleted` / `error` → `statusError`
|
||||
|
||||
## Overlays (dialogs, palette, tooltips)
|
||||
|
||||
```
|
||||
dialog bg → modalSurfaceBackground
|
||||
dialog border→ modalSurfaceBorder
|
||||
backdrop → modalOverlayBackground
|
||||
tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
||||
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- `globalBackground` for panel fill → use `panelBackground`
|
||||
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||
- Tab active bg = `panelBackground` → use `panelHeader` (elevated chrome)
|
||||
- Tab active border = `globalFocus` → use `panelActiveBorder`
|
||||
@@ -0,0 +1,70 @@
|
||||
# Theme — token system, palette, typography
|
||||
|
||||
## Token identity rule
|
||||
|
||||
Every visual surface gets its own named token. Never borrow a token from
|
||||
another surface just because they happen to resolve to the same color.
|
||||
|
||||
**Wrong:** `sidebarBackground` for the hat bar (the hat isn't a sidebar).
|
||||
**Right:** Create `chromeBackground` that resolves to the same palette key.
|
||||
|
||||
When two surfaces share a color:
|
||||
|
||||
1. **Same conceptual surface** (sidebar + context panel are both "side
|
||||
panels") → one shared token set is fine.
|
||||
2. **Different surfaces that happen to match** (hat bar + sidebar +
|
||||
status bar are all "chrome frame") → create a shared primitive in the
|
||||
palette/semantic layer (e.g. `bgChrome`) and give each surface its
|
||||
own token that maps to that primitive. This lets themes diverge them
|
||||
later without breaking widgets.
|
||||
|
||||
## Palette depth primitives
|
||||
|
||||
The palette layer has these depth primitives (defined in each theme YAML):
|
||||
|
||||
- `bg` (`#20202C`) — outermost root, behind everything
|
||||
- `bgSunken` (`#1A1A24`) — chrome frame: sidebar, hat, statusbar
|
||||
- `surface` (`#242838`) — elevated: pane headers, active tabs
|
||||
- `surfaceHi` (`#2C3046`) — interactive: hover states, selections
|
||||
|
||||
Chrome tokens (`chromeBackground` / `chromeForeground` / `chromeBorder`)
|
||||
are the shared root for all frame surfaces. They resolve to `bgSunken` /
|
||||
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||
hat from sidebar from status bar if desired.
|
||||
|
||||
## Typography
|
||||
|
||||
Three constants — never hardcode sizes or families:
|
||||
|
||||
```
|
||||
family UI → inherited from DefaultTextStyle (JosefinSans Light 300)
|
||||
family mono → clideMonoFamily (JetBrainsMono)
|
||||
body size → clideFontBody (15)
|
||||
caption size → clideFontCaption (14) — status bar, section headers, git info
|
||||
mono size → clideFontMono (14) — terminal, code, paths, IDs
|
||||
```
|
||||
|
||||
Use `ClideText` for themed text. Set `muted: true` for secondary text
|
||||
(resolves to `globalTextMuted`). Set `fontFamily: clideMonoFamily` for
|
||||
code/paths/IDs. Don't set fontFamily for UI text — it inherits.
|
||||
|
||||
## Extension-owned domain colors
|
||||
|
||||
Extensions that need domain-specific color coding (ticket types, decision
|
||||
types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead:
|
||||
|
||||
1. Create a color map class in the extension (e.g. `TicketTypeColors`).
|
||||
2. Ship dark and light presets, auto-selected via
|
||||
`ClideTheme.of(context).dark`.
|
||||
3. Store user overrides under `ext.<id>.colors` in settings.
|
||||
4. Reference: `lib/builtin/tickets/src/ticket_colors.dart`.
|
||||
|
||||
This keeps the core token surface lean and lets each extension own its
|
||||
palette. The pattern scales to any extension needing domain colors.
|
||||
|
||||
## Where to look in the codebase
|
||||
|
||||
- Full token list: `lib/kernel/src/theme/tokens.dart`
|
||||
- Resolver fallbacks: `lib/kernel/src/theme/resolver.dart`
|
||||
- Theme YAML example: `lib/kernel/src/theme/themes/clide.yaml`
|
||||
- Decision: D-43 (handoff), D-44 (four bundled themes), D-45 (syntax tokens)
|
||||
@@ -0,0 +1,240 @@
|
||||
---
|
||||
name: whats-next
|
||||
description: >
|
||||
Surface the best batch of tickets to pick up next from pql. Walks the
|
||||
initiative/epic tree, filters to unblocked work, refines context via
|
||||
parallel agents (or `pql ticket refine` for empty descriptions), and
|
||||
optionally activates the batch on a fresh branch. Use when the user
|
||||
says "what's next", "next batch", "pick up work", or invokes
|
||||
/whats-next. NOT triggered by "what should we work on" in a design
|
||||
context — that's a discussion, not a batch selection.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, Agent, AskUserQuestion
|
||||
---
|
||||
|
||||
# What's Next
|
||||
|
||||
Dependency-driven batch selection against pql. Three steps:
|
||||
batch selection → refinement review → batch activation.
|
||||
|
||||
Pql is the single source of truth for tickets and decisions in this repo
|
||||
(see [pql skill](../pql/SKILL.md) and [`decisions/README.md`](../../../decisions/README.md)). Always run from the repo root.
|
||||
|
||||
## Step 0: Sync state
|
||||
|
||||
Decisions on disk may be ahead of pql.db. Always sync before reading:
|
||||
|
||||
```bash
|
||||
pql decisions sync
|
||||
```
|
||||
|
||||
If `pql` is missing, stop and tell the user — don't fall back to grep.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Batch Selection
|
||||
|
||||
### 1a. Find active top-level work
|
||||
|
||||
Pql has no `milestone` concept; **initiatives** (and large **epics**) play
|
||||
that role. List in-flight top-level work:
|
||||
|
||||
```bash
|
||||
pql ticket list --status in_progress --pretty
|
||||
pql ticket list --status ready --pretty
|
||||
```
|
||||
|
||||
If nothing is `in_progress` or `ready` at the initiative/epic level,
|
||||
fall back to `pql plan status --pretty` for a dashboard read and ask the
|
||||
user which area to advance.
|
||||
|
||||
### 1b. Build the work landscape
|
||||
|
||||
For each candidate epic or initiative, expand its children:
|
||||
|
||||
```bash
|
||||
pql ticket show <id> --with-children --pretty
|
||||
```
|
||||
|
||||
Collect every leaf ticket (story/task/bug) underneath. Deduplicate.
|
||||
|
||||
### 1c. Filter to unblocked tickets
|
||||
|
||||
For each leaf with status `ready` or `backlog`, check blockers:
|
||||
|
||||
```bash
|
||||
pql ticket show <id> --with-blockers --pretty
|
||||
```
|
||||
|
||||
A ticket is **unblocked** if every blocker is `done` or `cancelled`.
|
||||
Drop the rest.
|
||||
|
||||
### 1d. Rank and group
|
||||
|
||||
Rank unblocked tickets by:
|
||||
|
||||
1. **Priority** (critical > high > medium > low) — read from ticket fields.
|
||||
2. **Epic proximity to done** — for each epic parent, compute
|
||||
`done_children / total_children`. Higher ratio ranks higher: finishing
|
||||
an epic unlocks downstream work and tightens the board.
|
||||
3. **Fan-out** — tickets that unblock the most other tickets rank higher.
|
||||
Approximate by scanning `pql ticket list --status backlog --pretty`
|
||||
and counting how many list this ticket in their blockers (use
|
||||
`--with-blockers` per candidate, or read `--jsonl` once and reduce in
|
||||
memory).
|
||||
|
||||
Group into **epic-sized batches**: tickets sharing a `parent_id`, or a
|
||||
logical cluster if no shared parent. If nothing groups naturally, batch
|
||||
by area (the directory the work touches, e.g. `lib/src/pty/`).
|
||||
|
||||
### 1e. Show the board
|
||||
|
||||
```bash
|
||||
pql ticket board --pretty
|
||||
```
|
||||
|
||||
This is the "what's currently in flight" view — the user wants to see
|
||||
WIP before committing to more.
|
||||
|
||||
### 1f. Present the recommended batch
|
||||
|
||||
Show the user:
|
||||
|
||||
- The recommended batch — IDs, titles, priorities, parent epic.
|
||||
- **Why this batch** — which epic it advances, what it unblocks downstream.
|
||||
- Current board state (WIP count vs. ready/backlog).
|
||||
- One or two alternative batches worth considering.
|
||||
|
||||
Wait for user confirmation before Step 2.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Ticket Refinement Review
|
||||
|
||||
Two cases — handle the cheap one first.
|
||||
|
||||
### 2a. Tickets with empty descriptions → use pql
|
||||
|
||||
If any ticket in the batch has no description, hand off to pql's
|
||||
built-in refinement flow:
|
||||
|
||||
```bash
|
||||
pql ticket refine list --pretty
|
||||
pql ticket refine next --pretty # full context for the next one
|
||||
pql ticket refine write T-NN '{"description":"..."}'
|
||||
```
|
||||
|
||||
Walk these with the user (AskUserQuestion per ticket if appropriate)
|
||||
before moving on.
|
||||
|
||||
### 2b. Tickets with descriptions → spawn refinement agents
|
||||
|
||||
For each ticket that already has a description but may still be
|
||||
under-specified, spawn one agent in parallel. Use `general-purpose`
|
||||
subagent_type (custom subagent_types lose SendMessage):
|
||||
|
||||
```
|
||||
Agent({
|
||||
subagent_type: "general-purpose",
|
||||
model: "sonnet",
|
||||
description: "Refine T-NN context",
|
||||
prompt: "You are the Refinement Manager for ticket T-NN.
|
||||
|
||||
Ticket: <title>
|
||||
Description: <body>
|
||||
Decision ref: <D-NN or Q-NN, if set>
|
||||
|
||||
Your job:
|
||||
1. Run `pql decisions show <decision_ref> --with-refs --pretty` and
|
||||
read the linked D/Q-record in decisions/<domain>.md.
|
||||
2. Grep decisions/questions-*.md for related Q-records.
|
||||
3. Verify referenced files, classes, and APIs actually exist in the
|
||||
current tree (Read/Grep). Flag dangling references.
|
||||
4. Cross-check against CLAUDE.md guardrails (single process, CLI-first,
|
||||
own the rendering stack, etc.) — flag tickets that conflict.
|
||||
|
||||
Assess: does an implementer have enough context to proceed without
|
||||
guessing? Report exactly one of:
|
||||
- READY: <one-paragraph summary of what the implementer needs to know>
|
||||
- GAPS: <list of specific ambiguities, each with 2–3 options>"
|
||||
})
|
||||
```
|
||||
|
||||
Run all agents in parallel (single message, multiple Agent tool calls).
|
||||
|
||||
### 2c. Resolve gaps
|
||||
|
||||
For each ticket that came back GAPS, surface ambiguities to the user
|
||||
via AskUserQuestion. After the user resolves, append the resolution to
|
||||
the ticket via pql:
|
||||
|
||||
```bash
|
||||
pql ticket refine write T-NN '{"description":"<existing body>\n\n---\nRefinement: <resolution>"}'
|
||||
```
|
||||
|
||||
If a gap really requires a new D-record (architectural choice, not just
|
||||
detail), flag it. Ask whether to write the D-record now (`pql decisions
|
||||
claim D <domain> "title"` then author the markdown) or defer with a note
|
||||
on the ticket.
|
||||
|
||||
### 2d. Present refined batch summary
|
||||
|
||||
Per ticket:
|
||||
|
||||
- READY summary, or the resolution the user just gave.
|
||||
- Linked D/Q-records.
|
||||
- Remaining blockers (should be none — re-check if Step 1 was a while ago).
|
||||
|
||||
Ask: "Batch ready. Activate?"
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Batch Activation
|
||||
|
||||
### 3a. Mark tickets in_progress
|
||||
|
||||
Batch transition (comma-separated IDs):
|
||||
|
||||
```bash
|
||||
pql ticket status T-1,T-2,T-3 in_progress
|
||||
```
|
||||
|
||||
### 3b. Branch? Default no.
|
||||
|
||||
Solo-dev flow on this repo — work lands directly on `main` (see recent
|
||||
`git log`). Don't create a topic branch unless the user explicitly asks.
|
||||
If they do, plain `git checkout -b` is fine; there is no `gh` CLI.
|
||||
|
||||
### 3c. Spawn implementation agents (optional)
|
||||
|
||||
If the user wants agents driving the work, spawn `general-purpose`
|
||||
subagents (`model: sonnet`) per ticket. Each prompt should include:
|
||||
|
||||
- Ticket details + the refinement summary from Step 2.
|
||||
- The full content of any linked D-record (Read it and inline it — don't
|
||||
just cite the ID; the agent has no project memory of it).
|
||||
- Repo guardrails the work touches (from CLAUDE.md — quote the relevant
|
||||
bullets, don't link).
|
||||
- A RULES block: write files only, no git commits, no destructive ops,
|
||||
message back when blocked or done.
|
||||
|
||||
### 3d. Report
|
||||
|
||||
End with a tight summary:
|
||||
|
||||
- Branch.
|
||||
- Tickets now `in_progress`.
|
||||
- Agents spawned (if any).
|
||||
- Next step: implement, then commit per the [git-commit skill](../git-commit/SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Don't skip Step 0 — stale `pql.db` makes the rest of the skill lie.
|
||||
- Don't activate a batch the user hasn't confirmed.
|
||||
- Don't spawn refinement agents for tickets that have no description — use
|
||||
`pql ticket refine` instead; it's cheaper and writes back through the
|
||||
proper channel.
|
||||
- Don't reach for `gh` — this system doesn't have it. Plain `git` only.
|
||||
- Don't `cd` into subdirectories — run everything from the repo root.
|
||||
@@ -0,0 +1 @@
|
||||
.pql/changelog/*.sql merge=union
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- run: (cd app && flutter pub get)
|
||||
- run: ci/test.sh
|
||||
- run: ci/test_a11y.sh
|
||||
- run: ci/test_coverage.sh
|
||||
- run: ci/coverage_gate.sh
|
||||
|
||||
integration:
|
||||
name: integration_test (xvfb)
|
||||
@@ -69,3 +69,24 @@ jobs:
|
||||
- run: (cd app && flutter pub get)
|
||||
- run: (cd tools/ui && npm install && npx playwright install --with-deps chromium)
|
||||
- run: ci/test_e2e.sh
|
||||
|
||||
docs:
|
||||
name: dart doc (lib API)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable, cache: true }
|
||||
- run: dart pub get
|
||||
- name: dart doc --validate-links (fail on warning)
|
||||
run: |
|
||||
set -o pipefail
|
||||
dart doc --validate-links 2>&1 | tee dartdoc.log
|
||||
if grep -q "^ warning:" dartdoc.log; then
|
||||
echo "::error::dartdoc emitted warnings — see log above"
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dart-api-docs
|
||||
path: doc/api/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/post-checkout (rebuild pql.db on branch checkout)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-checkout"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/post-merge (planning-state auto-import on pull)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-merge"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/post-rewrite (rebuild pql.db after rebase / amend)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-rewrite"
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/pre-commit (planning-state auto-export)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/pre-commit"
|
||||
@@ -26,13 +26,17 @@ tools/ui/test-results/
|
||||
tools/ui/playwright-report/
|
||||
tools/ui/.serve.pid
|
||||
|
||||
# -- ptyc (C supporter tool) --------------------------------------------
|
||||
/ptyc/bin/
|
||||
/ptyc/*.o
|
||||
|
||||
# -- dugite-native (bundled git, downloaded at build time) ---------------
|
||||
/native/dugite/
|
||||
|
||||
# -- dart doc output (generated by `dart doc`, uploaded as CI artefact) -
|
||||
/doc/
|
||||
|
||||
# -- coverage output (regenerated by every `flutter test --coverage`).
|
||||
# Floor lives in pubspec.yaml `coverage_floor:`; nothing under
|
||||
# coverage/ is committed.
|
||||
/coverage/
|
||||
|
||||
# -- Test, coverage, profile output ------------------------------------
|
||||
*.test
|
||||
*.out
|
||||
@@ -48,7 +52,7 @@ test/goldens/failures/
|
||||
|
||||
# -- pql per-repo state (index + planning DB, rebuilt from markdown) ----
|
||||
/.pql/*
|
||||
!/.pql/pql-plan.json
|
||||
!/.pql/changelog/
|
||||
|
||||
# -- SQLite index files (defensive; should never land at repo root) ----
|
||||
*.sqlite
|
||||
@@ -95,3 +99,8 @@ legacy/**/.coverage
|
||||
legacy/**/.coverage.*
|
||||
.clide/settings.yaml
|
||||
.claude/skills/pql/
|
||||
|
||||
# frame0-wireframe local ID ↔ Frame0 ID mapping; per-machine state.
|
||||
*.idmap.json
|
||||
.pql/*
|
||||
!.pql/changelog/
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Auto-generated by pql init. CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||
-- idempotent so running schema files from each directory in
|
||||
-- replay order is harmless.
|
||||
--
|
||||
-- Importer parses the markers below to detect schema drift
|
||||
-- between the producing pql version and the local one — a
|
||||
-- bumped canonical_version means projection rules changed
|
||||
-- and replay must refuse rather than silently corrupt state.
|
||||
-- pql:created_by: 1.4.26
|
||||
-- pql:canonical_version: 1
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||
domain TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK(status IN ('active','superseded','resolved','open')),
|
||||
date TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
ref_type TEXT NOT NULL
|
||||
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (source_id, target_id, ref_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||
parent_id TEXT REFERENCES tickets(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'backlog'
|
||||
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||
priority TEXT DEFAULT 'medium'
|
||||
CHECK(priority IN ('critical','high','medium','low')),
|
||||
assigned_to TEXT,
|
||||
team TEXT,
|
||||
decision_ref TEXT REFERENCES decisions(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (blocker_id, blocked_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by TEXT,
|
||||
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT UNIQUE,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
label TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (ticket_id, label)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Auto-generated by pql init. CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||
-- idempotent so running schema files from each directory in
|
||||
-- replay order is harmless.
|
||||
--
|
||||
-- Importer parses the markers below to detect schema drift
|
||||
-- between the producing pql version and the local one — a
|
||||
-- bumped canonical_version means projection rules changed
|
||||
-- and replay must refuse rather than silently corrupt state.
|
||||
-- pql:created_by: 1.4.26
|
||||
-- pql:canonical_version: 1
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||
domain TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK(status IN ('active','superseded','resolved','open')),
|
||||
date TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
ref_type TEXT NOT NULL
|
||||
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (source_id, target_id, ref_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||
parent_id TEXT REFERENCES tickets(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'backlog'
|
||||
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||
priority TEXT DEFAULT 'medium'
|
||||
CHECK(priority IN ('critical','high','medium','low')),
|
||||
assigned_to TEXT,
|
||||
team TEXT,
|
||||
decision_ref TEXT REFERENCES decisions(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (blocker_id, blocked_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by TEXT,
|
||||
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT UNIQUE,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
label TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (ticket_id, label)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||
@@ -0,0 +1,483 @@
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '66d7c49f6b2ccdd2ef88a635ec717ca2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '87c83225076b22ecbc7fd150b8ccce5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '056f21749ff40c14feeb380b2425bb04', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'a345d1db748f86fb707b715ce4f45c82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'd666982c7578a64cff619807467b0d22', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, 'bf092de792c1dba0a724e6c3ef0a2ca3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '6648704125e10ddf9582b0fc8a91cbb8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '28a5fbab4db244d0cc5c48e7e320ec71', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '8e8f387c6cf81e8db06c212be3c75ff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b0d4dd5c1779606c861d1fd778ee9d0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b93dd369ad1260e6b1f74798544bafd1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'e5b9592394cabd32ce426a3916c27827', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '140ed6257373fb7d199c67da8452bb82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '20cb7969c9c3af6a6deb93ee56a3a8f6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '456c32193f208cb4dcd78d722c8e5308', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '92282e57e514c08df603650c82fc5ad1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '2352d517e219af8edf4537f4145b5a9e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '37b7881534e16c2f369cdfb4a25ac511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7e4e0f286bd3bf1687cbbd49fca9e1f4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, 'c6ccbb7da6b303b5fd6ef4f5273c7d07', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '99d159170d35cb6225a0b9e6f8e5a4f5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '171dab623718a0cb2a4af914c84e0d30', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '206793ce446e6ba302f209cda04dcb78', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '2a311168aab1126eadcb2e23433ba317', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '3098b6064c8b96eb459c5ed947ca97c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '366f83e624514e442d539f704a0abbb1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36c5d3bbd5f027fdb114391abdf1dda6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '381444444f3e04024e2293f61c774d1d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '46d32785bb5113ebcb050845e67ea280', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5fde942d02640b9df4e75d1d745b2de5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '74dbfcfee3f60d46ba6e910ebb5fed1b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '7d19553f3714932803e565e8ac106220', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9de91c0384a97d66eaa01e7f8164f389', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c257d281cc9fc54be99fea49688ff720', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cf47b1a47c543dabd8fe0c891c535285', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd34d4be4170d93183c18754a60ea97e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'db0af7f083e8985026799a9d3334347d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '1a24e0b9fbc1472aacf17392a9241b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '7774f4c92e18371341b7c0332022d29a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '8589b12b48657f3513e801cd96dab11c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '92508b82168bd20d5a69c63a66271c34', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'dc2b5b3b5b348409f6d719f05130804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '0dd7c9716ba7dab383c7890822ba9224', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '23c19fa354da9eb201212b23696da027', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '79e01d297b5cf559d37f2c0ee3661a51', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '88a0fb2513eebbd4627abd854eefb484', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '9ad3112f08548b5896fd2252dda48be0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'acba548461502b88a483649f181a65da', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'bd986c8233a7a251c0403c4df3ce8511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'cfb6a1faab0ce219609f44055648cda1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'dabc0f13943decd855d39febfd2a18ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'e340923ef96f70f142a0bee868bb8e67', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'f179c71abcd89f7ed9fa781220d2018c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'fae80bffb40c02fdf07691ef18837b14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '1693e0a6448fb98bd03fe0c4c65371fb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '80d695ac3bfc5fe1f3615013dea3b06a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '33a14771999bc0062268447b4ffadc73', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '5a2f1c3d37090ad5791c6a0e8a7aa80a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '7d84a743a1b9e1ce5c50172b89318153', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'cad02006950fb6fa0d003553437d3bbe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '10f0a16fa4e4899c7817dd5c44cc3ffe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '2d697a30a6193eacf2a3ddf1661e861c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '591bb27186744d023df34105884fa58f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '817b9d8e92707eb588cb7f9eae7d424e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '5460d369f6cdef9c393a37e41481bcb5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '313d4c9dd6444a4573cbe9584ed18f61', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '4630269c56784316b975ca56b10a2337', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '5a8f7e0eda55bc0893c6597400a8d535', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'fac6b818dc68490a80ea32255f0ba404', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '0a7137f82fbb9307496b7e4133b15052', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '5aff1ed40d9a68a5b4d59aab0d24dd31', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '931bcaabbd64af0612fbe6eb9a4470d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'c2fca20d28b2a3a63a56eb2841a4bda9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '84e5b99f4fb99f8ae4d189ddaac2de74', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '8c7ba0fd4c8acc6065a24b80ae28d526', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'd91c6440f66e683a87be5e61a366cca7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'f9253848d9a95ce1823c8ba78a511fc0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '389d5abb35b3b73ccb38ac8bec3b92c7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '7e714e95f89966a13e436d2fe38e7443', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, 'b1aa8ff1f9b24cdfc9e0bebf21d38b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '39b407be88dbb31f530c8074f6bcecc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '59df9c23d722ccf1c1ac18baee76c785', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '76b48a80abcba29b1047c794fd004234', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '297763c026010189ea0bd870f77a173e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2cd947436be1c156eb77d233aeed5da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '38826870354a1bf51bc2ff21f6da0ea3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '5105038efff2e3055213b1b1403b7610', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '67cc6c0934711b6a592e11e5edc6194c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '6e9e7d3ce9f74ed261912b1cb099dcad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'a4e702776f3b553fb276782e539ebfc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'd294248ba8ea651e78aeb6fed0457eb4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'da8dc2002a03eaee7fcd5504abee6f60', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '08f09f3bb9f1a0001110716930824e62', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '10005b4803d743aa21be0223c8a85d5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '11ac23b546d0e22206af4221062f5912', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '127404fe79dfb79d0297b85d78235c68', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '15fb6606af61e11b6960799721e7cdcc', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '19f6209ce4428172fb16941accb13566', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-27', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '212224b7dcc4b991119dc430ae179311', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2fe8eed15b3ead999a69cdd08df5e14c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '3990623d651c604fec692edb70976d69', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '44d273b92ad4c93159706c1f17fcb6a7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '46935c5a6d8ef12cf1c2252bd8d30368', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '551ea05a8d0cc28c0ff7010a139e6fe1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6748fca610fc8a8503d69846231616fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '7843cfda82ec1c1622c1a201f82acaf1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '822790bbace926feb2372b8665080932', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9a1ae5742a1230e60279d9c72f8b0f75', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a414bc8e4d32fa32483228a4e9c7be8c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a80caae1913b8e9a1d5dab980bf0b77b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-40', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a895d51a2e6e8b448684933cf0224162', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ad9a8a85686e33df17ae15631afa1213', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'aedb3c83402245f81cd10f04d15dcf8e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b6dbb8114cd77c206d3b8bdf054736b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-26', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b7b189fe87f61d42e1d0318d23aeb1fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c80049d2c236f0d2512355183b766f0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-25', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ccdf7bec2a75f5829eab4fdd57171622', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-36', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'd13ed78c92d33e61e448c9e155adf870', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-41', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'eb2a2ca41a57604b40556e81bf6480a9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f0d27e423d40a998115e10ffd804f791', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f5c891c9a5f7d5e3f43ded0dc07eb704', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ff609c22f964618c84b7c36988e87ae7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '6ae864936d48565e880f06e984e9d978', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'ebebc38301a28001a09c9e7066c9aeb9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3cf128b79fe91447f72f42d8cffce9e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '68f57f5e3a1005ca3009f944b8b634d7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '50aa3e362dbe9682d98f6d5f71ea97b1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, '4cb951c3ea697881d8fa5b0e86cfdf96', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '7516ffa5c72fa4e7fd067249393c3653', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '268576bd9559485dd2ac5e7ffd1f1a79', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, 'ef46c3a3db4c4ba50ff1f0e1baa4c0e6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '1cd74d47eb547aa0bef71597eaaa2c5b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '3b8c17d366d1710d03523303c9df83c8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '7eb9ca56292802301f93d0e2d680835e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'a1a837494c047ade786d79edb75296dd', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, 'ac745b5254a0e8f085189c23d95acb7e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '2adaf942a49bfeeac90aee1527a98444', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'e0d54ffba34677dc6e687be801ccec05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '74e6fa2f524c21300467381c2f3ad4e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, 'fbd19eabf442e6e0de60028c28956b8f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7d3efe4ccaee7d45d1a01d28144c4f0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '914a2f5f5ef0807a6743f8811e71804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'a1cd74c76fd10c353aa758cea0331c5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'cdbe85f80838e781048780db934d0115', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f24e8d138172597d7ddcf26672b67d5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f9a83fa1b41d56202c57e6a55b240f35', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, '99c4e5a68463201e7f1b7764eddfd2ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, 'd0274058b130d8003123881b8a410f7f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '83d70eb5a7f6e7175e0719fdb23243c2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, 'b5acf95cb059f0c573b78d755b938da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, '4ae08b8af459af0679b05cf1f8ac3950', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, 'c7e4cabf5d1fccad28f22d498e91193a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '9b0937bc7eb82785407c367a90365c3a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '90587f61d96b12ce53f2a4edc705fdd8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, 'd02ac7ee61fc4dc420fb7597a4f326db', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, '145613c94b4366bda03fb76c0f0747ce', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '0d8e318e8060aece6e18788cee56f646', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '3d8635cfdbaf9b0c22dcba9f15f3c1ad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '77f873a7a5fff47ce9b974a6999d03b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, 'e7610d68ec9871746b4a361e33b18f05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '9c3afbbfca207aeb78541dbd9d90e462', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '66d7c49f6b2ccdd2ef88a635ec717ca2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '87c83225076b22ecbc7fd150b8ccce5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '056f21749ff40c14feeb380b2425bb04', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'a345d1db748f86fb707b715ce4f45c82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'd666982c7578a64cff619807467b0d22', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, 'bf092de792c1dba0a724e6c3ef0a2ca3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '6648704125e10ddf9582b0fc8a91cbb8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '28a5fbab4db244d0cc5c48e7e320ec71', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '8e8f387c6cf81e8db06c212be3c75ff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b0d4dd5c1779606c861d1fd778ee9d0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b93dd369ad1260e6b1f74798544bafd1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'e5b9592394cabd32ce426a3916c27827', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '140ed6257373fb7d199c67da8452bb82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '20cb7969c9c3af6a6deb93ee56a3a8f6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '456c32193f208cb4dcd78d722c8e5308', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '92282e57e514c08df603650c82fc5ad1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '2352d517e219af8edf4537f4145b5a9e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '37b7881534e16c2f369cdfb4a25ac511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7e4e0f286bd3bf1687cbbd49fca9e1f4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, 'c6ccbb7da6b303b5fd6ef4f5273c7d07', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '99d159170d35cb6225a0b9e6f8e5a4f5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '171dab623718a0cb2a4af914c84e0d30', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '206793ce446e6ba302f209cda04dcb78', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '2a311168aab1126eadcb2e23433ba317', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '3098b6064c8b96eb459c5ed947ca97c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '366f83e624514e442d539f704a0abbb1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36c5d3bbd5f027fdb114391abdf1dda6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '381444444f3e04024e2293f61c774d1d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '46d32785bb5113ebcb050845e67ea280', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5fde942d02640b9df4e75d1d745b2de5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '74dbfcfee3f60d46ba6e910ebb5fed1b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '7d19553f3714932803e565e8ac106220', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9de91c0384a97d66eaa01e7f8164f389', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c257d281cc9fc54be99fea49688ff720', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cf47b1a47c543dabd8fe0c891c535285', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd34d4be4170d93183c18754a60ea97e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'db0af7f083e8985026799a9d3334347d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '1a24e0b9fbc1472aacf17392a9241b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '7774f4c92e18371341b7c0332022d29a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '8589b12b48657f3513e801cd96dab11c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '92508b82168bd20d5a69c63a66271c34', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'dc2b5b3b5b348409f6d719f05130804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '0dd7c9716ba7dab383c7890822ba9224', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '23c19fa354da9eb201212b23696da027', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '79e01d297b5cf559d37f2c0ee3661a51', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '88a0fb2513eebbd4627abd854eefb484', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '9ad3112f08548b5896fd2252dda48be0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'acba548461502b88a483649f181a65da', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'bd986c8233a7a251c0403c4df3ce8511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'cfb6a1faab0ce219609f44055648cda1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'dabc0f13943decd855d39febfd2a18ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'e340923ef96f70f142a0bee868bb8e67', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'f179c71abcd89f7ed9fa781220d2018c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'fae80bffb40c02fdf07691ef18837b14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '1693e0a6448fb98bd03fe0c4c65371fb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '80d695ac3bfc5fe1f3615013dea3b06a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '33a14771999bc0062268447b4ffadc73', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '5a2f1c3d37090ad5791c6a0e8a7aa80a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '7d84a743a1b9e1ce5c50172b89318153', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'cad02006950fb6fa0d003553437d3bbe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '10f0a16fa4e4899c7817dd5c44cc3ffe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '2d697a30a6193eacf2a3ddf1661e861c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '591bb27186744d023df34105884fa58f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '817b9d8e92707eb588cb7f9eae7d424e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '5460d369f6cdef9c393a37e41481bcb5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '313d4c9dd6444a4573cbe9584ed18f61', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '4630269c56784316b975ca56b10a2337', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '5a8f7e0eda55bc0893c6597400a8d535', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'fac6b818dc68490a80ea32255f0ba404', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '0a7137f82fbb9307496b7e4133b15052', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '5aff1ed40d9a68a5b4d59aab0d24dd31', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '931bcaabbd64af0612fbe6eb9a4470d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'c2fca20d28b2a3a63a56eb2841a4bda9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '84e5b99f4fb99f8ae4d189ddaac2de74', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '8c7ba0fd4c8acc6065a24b80ae28d526', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'd91c6440f66e683a87be5e61a366cca7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'f9253848d9a95ce1823c8ba78a511fc0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '389d5abb35b3b73ccb38ac8bec3b92c7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '7e714e95f89966a13e436d2fe38e7443', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, 'b1aa8ff1f9b24cdfc9e0bebf21d38b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '39b407be88dbb31f530c8074f6bcecc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '59df9c23d722ccf1c1ac18baee76c785', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '76b48a80abcba29b1047c794fd004234', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '297763c026010189ea0bd870f77a173e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2cd947436be1c156eb77d233aeed5da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '38826870354a1bf51bc2ff21f6da0ea3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '5105038efff2e3055213b1b1403b7610', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '67cc6c0934711b6a592e11e5edc6194c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '6e9e7d3ce9f74ed261912b1cb099dcad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'a4e702776f3b553fb276782e539ebfc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'd294248ba8ea651e78aeb6fed0457eb4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'da8dc2002a03eaee7fcd5504abee6f60', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '08f09f3bb9f1a0001110716930824e62', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '10005b4803d743aa21be0223c8a85d5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '11ac23b546d0e22206af4221062f5912', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '127404fe79dfb79d0297b85d78235c68', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '15fb6606af61e11b6960799721e7cdcc', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '19f6209ce4428172fb16941accb13566', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-27', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '212224b7dcc4b991119dc430ae179311', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2fe8eed15b3ead999a69cdd08df5e14c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '3990623d651c604fec692edb70976d69', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '44d273b92ad4c93159706c1f17fcb6a7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '46935c5a6d8ef12cf1c2252bd8d30368', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '551ea05a8d0cc28c0ff7010a139e6fe1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6748fca610fc8a8503d69846231616fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '7843cfda82ec1c1622c1a201f82acaf1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '822790bbace926feb2372b8665080932', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9a1ae5742a1230e60279d9c72f8b0f75', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a414bc8e4d32fa32483228a4e9c7be8c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a80caae1913b8e9a1d5dab980bf0b77b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-40', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a895d51a2e6e8b448684933cf0224162', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ad9a8a85686e33df17ae15631afa1213', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'aedb3c83402245f81cd10f04d15dcf8e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b6dbb8114cd77c206d3b8bdf054736b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-26', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b7b189fe87f61d42e1d0318d23aeb1fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c80049d2c236f0d2512355183b766f0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-25', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ccdf7bec2a75f5829eab4fdd57171622', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-36', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'd13ed78c92d33e61e448c9e155adf870', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-41', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'eb2a2ca41a57604b40556e81bf6480a9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f0d27e423d40a998115e10ffd804f791', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f5c891c9a5f7d5e3f43ded0dc07eb704', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ff609c22f964618c84b7c36988e87ae7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '6ae864936d48565e880f06e984e9d978', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'ebebc38301a28001a09c9e7066c9aeb9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3cf128b79fe91447f72f42d8cffce9e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '68f57f5e3a1005ca3009f944b8b634d7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '50aa3e362dbe9682d98f6d5f71ea97b1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, '4cb951c3ea697881d8fa5b0e86cfdf96', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '7516ffa5c72fa4e7fd067249393c3653', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '268576bd9559485dd2ac5e7ffd1f1a79', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, 'ef46c3a3db4c4ba50ff1f0e1baa4c0e6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '1cd74d47eb547aa0bef71597eaaa2c5b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '3b8c17d366d1710d03523303c9df83c8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '7eb9ca56292802301f93d0e2d680835e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'a1a837494c047ade786d79edb75296dd', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, 'ac745b5254a0e8f085189c23d95acb7e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '2adaf942a49bfeeac90aee1527a98444', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'e0d54ffba34677dc6e687be801ccec05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '74e6fa2f524c21300467381c2f3ad4e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, 'fbd19eabf442e6e0de60028c28956b8f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7d3efe4ccaee7d45d1a01d28144c4f0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '914a2f5f5ef0807a6743f8811e71804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'a1cd74c76fd10c353aa758cea0331c5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'cdbe85f80838e781048780db934d0115', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f24e8d138172597d7ddcf26672b67d5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f9a83fa1b41d56202c57e6a55b240f35', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, '99c4e5a68463201e7f1b7764eddfd2ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, 'd0274058b130d8003123881b8a410f7f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '83d70eb5a7f6e7175e0719fdb23243c2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, 'b5acf95cb059f0c573b78d755b938da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, '4ae08b8af459af0679b05cf1f8ac3950', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, 'c7e4cabf5d1fccad28f22d498e91193a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '9b0937bc7eb82785407c367a90365c3a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '90587f61d96b12ce53f2a4edc705fdd8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, 'd02ac7ee61fc4dc420fb7597a4f326db', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, '145613c94b4366bda03fb76c0f0747ce', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '0d8e318e8060aece6e18788cee56f646', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '3d8635cfdbaf9b0c22dcba9f15f3c1ad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '77f873a7a5fff47ce9b974a6999d03b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, 'e7610d68ec9871746b4a361e33b18f05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '9c3afbbfca207aeb78541dbd9d90e462', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '66d7c49f6b2ccdd2ef88a635ec717ca2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '87c83225076b22ecbc7fd150b8ccce5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '056f21749ff40c14feeb380b2425bb04', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'a345d1db748f86fb707b715ce4f45c82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'd666982c7578a64cff619807467b0d22', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, 'bf092de792c1dba0a724e6c3ef0a2ca3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '6648704125e10ddf9582b0fc8a91cbb8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-2', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '28a5fbab4db244d0cc5c48e7e320ec71', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '8e8f387c6cf81e8db06c212be3c75ff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b0d4dd5c1779606c861d1fd778ee9d0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'b93dd369ad1260e6b1f74798544bafd1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-3', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'e5b9592394cabd32ce426a3916c27827', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '140ed6257373fb7d199c67da8452bb82', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '20cb7969c9c3af6a6deb93ee56a3a8f6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '456c32193f208cb4dcd78d722c8e5308', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-4', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '92282e57e514c08df603650c82fc5ad1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '2352d517e219af8edf4537f4145b5a9e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '37b7881534e16c2f369cdfb4a25ac511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7e4e0f286bd3bf1687cbbd49fca9e1f4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-5', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, 'c6ccbb7da6b303b5fd6ef4f5273c7d07', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '99d159170d35cb6225a0b9e6f8e5a4f5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '171dab623718a0cb2a4af914c84e0d30', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '206793ce446e6ba302f209cda04dcb78', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '2a311168aab1126eadcb2e23433ba317', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '3098b6064c8b96eb459c5ed947ca97c4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '366f83e624514e442d539f704a0abbb1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36c5d3bbd5f027fdb114391abdf1dda6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '381444444f3e04024e2293f61c774d1d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '46d32785bb5113ebcb050845e67ea280', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5fde942d02640b9df4e75d1d745b2de5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '74dbfcfee3f60d46ba6e910ebb5fed1b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '7d19553f3714932803e565e8ac106220', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9de91c0384a97d66eaa01e7f8164f389', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c257d281cc9fc54be99fea49688ff720', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cf47b1a47c543dabd8fe0c891c535285', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd34d4be4170d93183c18754a60ea97e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'db0af7f083e8985026799a9d3334347d', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '1a24e0b9fbc1472aacf17392a9241b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '7774f4c92e18371341b7c0332022d29a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '8589b12b48657f3513e801cd96dab11c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '92508b82168bd20d5a69c63a66271c34', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'dc2b5b3b5b348409f6d719f05130804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '0dd7c9716ba7dab383c7890822ba9224', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '23c19fa354da9eb201212b23696da027', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '79e01d297b5cf559d37f2c0ee3661a51', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '88a0fb2513eebbd4627abd854eefb484', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '9ad3112f08548b5896fd2252dda48be0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'acba548461502b88a483649f181a65da', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'bd986c8233a7a251c0403c4df3ce8511', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'cfb6a1faab0ce219609f44055648cda1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'dabc0f13943decd855d39febfd2a18ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'e340923ef96f70f142a0bee868bb8e67', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'f179c71abcd89f7ed9fa781220d2018c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'fae80bffb40c02fdf07691ef18837b14', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '1693e0a6448fb98bd03fe0c4c65371fb', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-6', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '80d695ac3bfc5fe1f3615013dea3b06a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '33a14771999bc0062268447b4ffadc73', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '5a2f1c3d37090ad5791c6a0e8a7aa80a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '7d84a743a1b9e1ce5c50172b89318153', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'cad02006950fb6fa0d003553437d3bbe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '10f0a16fa4e4899c7817dd5c44cc3ffe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '2d697a30a6193eacf2a3ddf1661e861c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '591bb27186744d023df34105884fa58f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '817b9d8e92707eb588cb7f9eae7d424e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '5460d369f6cdef9c393a37e41481bcb5', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '313d4c9dd6444a4573cbe9584ed18f61', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '4630269c56784316b975ca56b10a2337', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '5a8f7e0eda55bc0893c6597400a8d535', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'fac6b818dc68490a80ea32255f0ba404', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '0a7137f82fbb9307496b7e4133b15052', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '5aff1ed40d9a68a5b4d59aab0d24dd31', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '931bcaabbd64af0612fbe6eb9a4470d6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'c2fca20d28b2a3a63a56eb2841a4bda9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-31', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '84e5b99f4fb99f8ae4d189ddaac2de74', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-34', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '8c7ba0fd4c8acc6065a24b80ae28d526', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-30', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'd91c6440f66e683a87be5e61a366cca7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-33', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, 'f9253848d9a95ce1823c8ba78a511fc0', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '389d5abb35b3b73ccb38ac8bec3b92c7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '7e714e95f89966a13e436d2fe38e7443', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, 'b1aa8ff1f9b24cdfc9e0bebf21d38b99', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '39b407be88dbb31f530c8074f6bcecc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '59df9c23d722ccf1c1ac18baee76c785', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-32', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '76b48a80abcba29b1047c794fd004234', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '297763c026010189ea0bd870f77a173e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2cd947436be1c156eb77d233aeed5da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '38826870354a1bf51bc2ff21f6da0ea3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '5105038efff2e3055213b1b1403b7610', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '67cc6c0934711b6a592e11e5edc6194c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '6e9e7d3ce9f74ed261912b1cb099dcad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'a4e702776f3b553fb276782e539ebfc7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'd294248ba8ea651e78aeb6fed0457eb4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'da8dc2002a03eaee7fcd5504abee6f60', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '08f09f3bb9f1a0001110716930824e62', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-29', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '10005b4803d743aa21be0223c8a85d5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '11ac23b546d0e22206af4221062f5912', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-21', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '127404fe79dfb79d0297b85d78235c68', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-23', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '15fb6606af61e11b6960799721e7cdcc', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-38', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '19f6209ce4428172fb16941accb13566', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-27', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '212224b7dcc4b991119dc430ae179311', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-10', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2fe8eed15b3ead999a69cdd08df5e14c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-15', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '3990623d651c604fec692edb70976d69', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '44d273b92ad4c93159706c1f17fcb6a7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-19', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '46935c5a6d8ef12cf1c2252bd8d30368', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-37', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '551ea05a8d0cc28c0ff7010a139e6fe1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-14', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6748fca610fc8a8503d69846231616fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-20', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '7843cfda82ec1c1622c1a201f82acaf1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-13', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '822790bbace926feb2372b8665080932', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-22', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9a1ae5742a1230e60279d9c72f8b0f75', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a414bc8e4d32fa32483228a4e9c7be8c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-24', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a80caae1913b8e9a1d5dab980bf0b77b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-40', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'a895d51a2e6e8b448684933cf0224162', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ad9a8a85686e33df17ae15631afa1213', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-9', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'aedb3c83402245f81cd10f04d15dcf8e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-16', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b6dbb8114cd77c206d3b8bdf054736b9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-26', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b7b189fe87f61d42e1d0318d23aeb1fe', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-11', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c80049d2c236f0d2512355183b766f0f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-25', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ccdf7bec2a75f5829eab4fdd57171622', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-36', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'd13ed78c92d33e61e448c9e155adf870', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-41', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'eb2a2ca41a57604b40556e81bf6480a9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-35', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f0d27e423d40a998115e10ffd804f791', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-39', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f5c891c9a5f7d5e3f43ded0dc07eb704', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-12', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ff609c22f964618c84b7c36988e87ae7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '6ae864936d48565e880f06e984e9d978', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'ebebc38301a28001a09c9e7066c9aeb9', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3cf128b79fe91447f72f42d8cffce9e1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '68f57f5e3a1005ca3009f944b8b634d7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '50aa3e362dbe9682d98f6d5f71ea97b1', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, '4cb951c3ea697881d8fa5b0e86cfdf96', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '7516ffa5c72fa4e7fd067249393c3653', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '268576bd9559485dd2ac5e7ffd1f1a79', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, 'ef46c3a3db4c4ba50ff1f0e1baa4c0e6', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '1cd74d47eb547aa0bef71597eaaa2c5b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '3b8c17d366d1710d03523303c9df83c8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '7eb9ca56292802301f93d0e2d680835e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'a1a837494c047ade786d79edb75296dd', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, 'ac745b5254a0e8f085189c23d95acb7e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '2adaf942a49bfeeac90aee1527a98444', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'e0d54ffba34677dc6e687be801ccec05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '74e6fa2f524c21300467381c2f3ad4e3', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-17', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, 'fbd19eabf442e6e0de60028c28956b8f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7d3efe4ccaee7d45d1a01d28144c4f0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '914a2f5f5ef0807a6743f8811e71804b', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'a1cd74c76fd10c353aa758cea0331c5c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'cdbe85f80838e781048780db934d0115', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f24e8d138172597d7ddcf26672b67d5f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'f9a83fa1b41d56202c57e6a55b240f35', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, '99c4e5a68463201e7f1b7764eddfd2ae', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-18', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, 'd0274058b130d8003123881b8a410f7f', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '83d70eb5a7f6e7175e0719fdb23243c2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, 'b5acf95cb059f0c573b78d755b938da2', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, '4ae08b8af459af0679b05cf1f8ac3950', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, 'c7e4cabf5d1fccad28f22d498e91193a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '9b0937bc7eb82785407c367a90365c3a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '90587f61d96b12ce53f2a4edc705fdd8', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, 'd02ac7ee61fc4dc420fb7597a4f326db', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, '145613c94b4366bda03fb76c0f0747ce', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '0d8e318e8060aece6e18788cee56f646', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '3d8635cfdbaf9b0c22dcba9f15f3c1ad', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-63', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '77f873a7a5fff47ce9b974a6999d03b4', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-28', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, 'e7610d68ec9871746b4a361e33b18f05', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-43', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '9c3afbbfca207aeb78541dbd9d90e462', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Auto-generated by pql init. CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||
-- idempotent so running schema files from each directory in
|
||||
-- replay order is harmless.
|
||||
--
|
||||
-- Importer parses the markers below to detect schema drift
|
||||
-- between the producing pql version and the local one — a
|
||||
-- bumped canonical_version means projection rules changed
|
||||
-- and replay must refuse rather than silently corrupt state.
|
||||
-- pql:created_by: 1.4.26
|
||||
-- pql:canonical_version: 1
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||
domain TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK(status IN ('active','superseded','resolved','open')),
|
||||
date TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
ref_type TEXT NOT NULL
|
||||
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (source_id, target_id, ref_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||
parent_id TEXT REFERENCES tickets(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'backlog'
|
||||
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||
priority TEXT DEFAULT 'medium'
|
||||
CHECK(priority IN ('critical','high','medium','low')),
|
||||
assigned_to TEXT,
|
||||
team TEXT,
|
||||
decision_ref TEXT REFERENCES decisions(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (blocker_id, blocked_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by TEXT,
|
||||
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT UNIQUE,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
label TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (ticket_id, label)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||
@@ -0,0 +1,114 @@
|
||||
-- Auto-generated by pql init. CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15. CREATE TABLE IF NOT EXISTS is
|
||||
-- idempotent so running schema files from each directory in
|
||||
-- replay order is harmless.
|
||||
--
|
||||
-- Importer parses the markers below to detect schema drift
|
||||
-- between the producing pql version and the local one — a
|
||||
-- bumped canonical_version means projection rules changed
|
||||
-- and replay must refuse rather than silently corrupt state.
|
||||
-- pql:created_by: 1.4.26
|
||||
-- pql:canonical_version: 1
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('confirmed','question','rejected')),
|
||||
domain TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK(status IN ('active','superseded','resolved','open')),
|
||||
date TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
synced_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS decision_refs (
|
||||
source_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
target_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE CASCADE,
|
||||
ref_type TEXT NOT NULL
|
||||
CHECK(ref_type IN ('supersedes','references','resolves','depends_on','amends')),
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (source_id, target_id, ref_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL CHECK(type IN ('initiative','epic','story','task','bug')),
|
||||
parent_id TEXT REFERENCES tickets(id),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'backlog'
|
||||
CHECK(status IN ('backlog','ready','in_progress','review','done','cancelled')),
|
||||
priority TEXT DEFAULT 'medium'
|
||||
CHECK(priority IN ('critical','high','medium','low')),
|
||||
assigned_to TEXT,
|
||||
team TEXT,
|
||||
decision_ref TEXT REFERENCES decisions(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_deps (
|
||||
blocker_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
blocked_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (blocker_id, blocked_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_history (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
field TEXT NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_by TEXT,
|
||||
changed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT UNIQUE,
|
||||
canonical_version INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ticket_labels (
|
||||
ticket_id TEXT NOT NULL REFERENCES tickets(id),
|
||||
label TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT,
|
||||
hash TEXT,
|
||||
canonical_version INTEGER,
|
||||
PRIMARY KEY (ticket_id, label)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_team ON tickets(team);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_decision_ref ON tickets(decision_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_tickets_assigned ON tickets(assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_domain ON decisions(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_decisions_type ON decisions(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_decision_refs_target ON decision_refs(target_id);
|
||||
@@ -16,6 +16,170 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Removed
|
||||
|
||||
- **`bin/clide.dart` + `DaemonServer`** — completing the D-56 dissolution.
|
||||
The separate daemon process was dissolved on 2026-04-23 but the entry
|
||||
point and socket server class were never actually deleted. Gone now,
|
||||
along with orphaned tests (`test/cli/`, `test/daemon/subprocess_test`,
|
||||
`test/daemon/in_process_test`), stale i18n strings, and "start
|
||||
`clide --daemon`" error messages.
|
||||
- **`ptyc/` source tree + `PtySession` + `scm_rights.dart`** — PTY
|
||||
spawning migrated to Dart FFI `forkpty()` (`NativePty`) but the old
|
||||
C helper and its Dart wiring were never cleaned up. Removed from
|
||||
toolchain resolution, `ToolCheck` gate, backend serialization,
|
||||
testmode harness, CI scripts, Makefile, and sandbox entitlements.
|
||||
D-5 amended to record the retirement.
|
||||
- CI golden images (`test/goldens/goldens/ci/`) — Skia anti-aliasing
|
||||
of geometric shapes differs between macOS and Linux even with the
|
||||
Ahem font, so a single set of CI goldens can't serve both platforms.
|
||||
Replaced with platform-keyed goldens (`goldens/linux/`,
|
||||
`goldens/macos/`), each only compared on its own OS.
|
||||
|
||||
### Added
|
||||
|
||||
- Pre-push coverage gate — `make push-check` (and the
|
||||
`.githooks/pre-push` hook that calls it) now runs
|
||||
`ci/coverage_gate.sh`, which fails if total line coverage drops
|
||||
below the `coverage_floor:` value in `pubspec.yaml`. The floor
|
||||
starts at 34% (today's measured floor) and only ratchets up; the
|
||||
end target is 95% (D-66). `ci/test.sh` now writes
|
||||
`coverage/lcov.info` as a side effect of the unit/widget/golden
|
||||
run so the gate adds no extra test invocation.
|
||||
- Staged `dart doc` CI job — generates and uploads an HTML API
|
||||
reference for the public `lib/` surface. The step wraps
|
||||
`dart doc --validate-links` and grep-fails the build on any warning,
|
||||
so broken doc refs and dangling links can't accumulate. Inert with
|
||||
the rest of the workflow until Gitea Actions activates.
|
||||
- Mouse wheel scrolling in Claude pane — converts scroll events to
|
||||
PgUp/PgDown so Claude Code (and other TUI apps) scroll their
|
||||
history naturally.
|
||||
- Welcome screen Tips card — six common keybindings shown below the
|
||||
START / RECENT row when the viewport is tall enough.
|
||||
- `MultitabPane` widget + `MultitabController` for panes that host
|
||||
N runtime tab instances of the same kind. Generic over a payload
|
||||
type, supports pinned/non-closeable tabs (primary), drag-reorder,
|
||||
close × on hover, and an optional `+` add button. Used by the
|
||||
Claude pane to render primary + secondaries.
|
||||
- `MultitabPane.keepAlive` mode — when set, all entry bodies stay
|
||||
mounted via IndexedStack so switching tabs preserves their state
|
||||
(PTY connections, scroll position, etc.).
|
||||
|
||||
### Fixed
|
||||
|
||||
- `TerminalView.onTapUp` callbacks now actually fire on a primary
|
||||
tap. The parameter was wired to a code path that nothing in the
|
||||
gesture-detector chain ever invoked — the documented
|
||||
"Callback for when the user taps on the terminal" was a no-op
|
||||
for every caller. Routes through `TerminalGestureHandler.onSingleTapUp`
|
||||
now, with the resolved cell offset (T-93). Dead `onTapUp` surface
|
||||
on `TerminalGestureHandler` and `TerminalGestureDetector` removed
|
||||
in the same change.
|
||||
- `BufferLine.eraseRange` no longer panics when called with `end == 0`.
|
||||
The right-side wide-char guard read `_data[-1]` via `getWidth(-1)`,
|
||||
which threw a `RangeError`. Real trigger path: `Terminal.eraseDisplayAbove`
|
||||
with the cursor at column 0 — common after `ESC[H\x1b[1J`
|
||||
(home + erase-above) sequences that many TUIs emit on redraw.
|
||||
- Terminal selections no longer silently disappear when the terminal is
|
||||
resized narrower. Reflow's tail-anchor handler used to reparent
|
||||
anchors past the source's trimmed-content range onto a builder line
|
||||
that was never emitted, leaving them detached from the visible
|
||||
buffer; the selection controller's `extent.attached` check then
|
||||
returned null and the highlight vanished. Common trigger paths:
|
||||
Ctrl+A (select-all) followed by a width change, and mouse drag
|
||||
selections that extended past the end of a partially-filled line
|
||||
(T-92).
|
||||
- `BufferLine.removeCells` / `insertCells` / `dispose` no longer skip
|
||||
anchors due to concurrent list modification during iteration —
|
||||
surfaced by unit tests added under T-91. Anchors disposed inside
|
||||
the loop were unhooking themselves from the same list the loop was
|
||||
iterating, causing later anchors to be silently skipped (no
|
||||
reposition, no dispose) and leaving the buffer in an inconsistent
|
||||
state. Iteration now snapshots the list first.
|
||||
|
||||
### Changed
|
||||
|
||||
- Terminal panes now render bold attributes with a real bold weight —
|
||||
bundled JetBrainsMono Bold + BoldItalic are registered with the
|
||||
`JetBrainsMono` family at `weight: 700`. The painter's bold
|
||||
suppression workaround (added when only Regular + Italic were wired
|
||||
and Flutter's synthetic bold drifted advance widths) is gone.
|
||||
- Claude pane uses `MultitabPane` for primary + secondaries — drops
|
||||
~100 lines of bespoke tab-strip code, gains drag-to-reorder.
|
||||
- UI spacing constants live in `lib/widgets/src/spacing.dart` —
|
||||
`clideInset*` for paddings, `clideGap*` for sibling distances,
|
||||
`clideIcon*` / `clideControlHeight` for control sizes. Inline
|
||||
pixel literals replaced where they were repeated.
|
||||
|
||||
### Changed
|
||||
|
||||
- Tagline reads "IDE for Claude Code CLI" everywhere (welcome
|
||||
subtitle, README, CLAUDE.md, pubspec, web manifest, CLI banner).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Closing a secondary Claude pane tab now kills its tmux session
|
||||
on the clide socket, honouring D-41's "closing a secondary kills
|
||||
that tmux session" lifecycle. Previously `pane.close` only killed
|
||||
the ptyc-spawned tmux client and the server-side session leaked.
|
||||
- Cold-start reap: every clide launch kills any leftover secondary
|
||||
tmux sessions for the current repo before spawning new ones, so
|
||||
D-41's "secondary numbering resets between runs" holds even after
|
||||
an abrupt previous exit (kill -9, crash, force-quit).
|
||||
- `claude.kill-all-sessions` command now actually kills the
|
||||
server-side tmux sessions for the repo, not just the panes.
|
||||
- Terminal cell grid no longer drifts on bold text — bold rendering
|
||||
is suppressed at the painter level since synthetic bold (with no
|
||||
Bold.ttf registered) shifts glyph advance widths.
|
||||
- PTY surfaces errno on `forkpty`, `write`, and `ioctl` failures
|
||||
instead of swallowing them. `execve` failures in the spawned
|
||||
child now write a diagnostic line to the slave PTY before
|
||||
`_exit`, so the parent's reader sees the cause instead of an
|
||||
immediate EOF that looked indistinguishable from clean exit.
|
||||
PTY `write` loops on short writes; both `NativePty.write` and
|
||||
`PtySession.write` now throw `PtyException` on hard errors.
|
||||
- PTY teardown order fixed — kill the child first so the master
|
||||
fd returns EOF, await the reader isolate exit, then close the
|
||||
fd. Previously closing the fd while the isolate still polled it
|
||||
could briefly target a reused fd. Reader isolate spawn errors
|
||||
in both `NativePty` and `PtySession` are now surfaced via the
|
||||
output stream instead of silently dropped. `_recvFdAsync` no
|
||||
longer leaks the `ReceivePort` when `Isolate.spawn` throws, and
|
||||
`PtySession.spawn` closes the master fd if any post-receive
|
||||
step fails.
|
||||
- IPC server hardening: per-request 60s timeout (configurable),
|
||||
broadcast/response write failures logged instead of swallowed,
|
||||
client dropped on response-write failure, and the stale-socket
|
||||
retry now probes for a live daemon before unlinking the socket
|
||||
(refusing to start if one answers).
|
||||
- `pane.spawn` and `editor.open` now map POSIX errno values to
|
||||
actionable IPC error kinds. ENOENT → `not_found`, EACCES/EPERM
|
||||
→ `user_error` with a permissions hint, EISDIR/ENOTDIR/EEXIST
|
||||
→ distinct user-error/conflict, EMFILE/ENFILE → `tool_error`
|
||||
with a "fd limit hit" hint. Previously every spawn/open failure
|
||||
was an indistinguishable `tool_error`.
|
||||
|
||||
### Security
|
||||
|
||||
- `files.read` and `files.ls` now reject paths that resolve outside
|
||||
the workspace root. Previously a relative path containing `..`
|
||||
could read arbitrary files via path traversal.
|
||||
|
||||
### Changed
|
||||
|
||||
- Inline terminal emulator based on xterm.dart v4.0.0 — replaces the
|
||||
pub.dev dependency with owned code under `lib/src/terminal/`. Drops
|
||||
three transitive dependencies (xterm, quiver, zmodem).
|
||||
- Bundle clide-specific tmux.conf for Claude pane sessions: no status
|
||||
bar, 50k scrollback, mouse on, zero escape delay, isolated socket.
|
||||
- Claude pane spawns `claude` directly inside tmux with
|
||||
`CLAUDE_CODE_NO_FLICKER=1` to enable Claude's fullscreen TUI mode
|
||||
(input box pinned at the bottom).
|
||||
- PTY read buffer increased from 4KB to 64KB.
|
||||
- Terminal view 2px padding on all sides.
|
||||
- Remove bold JetBrains Mono font registration to prevent glyph width
|
||||
mismatch in terminal rendering.
|
||||
|
||||
## [2.0.0] — 2026-05-03
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -4,30 +4,30 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## What clide is
|
||||
|
||||
A Flutter desktop IDE for Claude Code. Single Flutter package at the repo root, plus small native supporter tools where Dart can't reach.
|
||||
An IDE for Claude Code CLI. Single Flutter package at the repo root.
|
||||
|
||||
- **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56).
|
||||
- **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56). PTY spawning uses Dart FFI `forkpty()` directly.
|
||||
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||
- **`ptyc/`** — small C supporter tool, peer of pql. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Clide shells out to it for every pane (shell, tmux, claude, LSP, debug adapter).
|
||||
|
||||
tmux owns Claude session persistence (D-41) — the app re-attaches on restart via `tmux new-session -A`. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
||||
|
||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`decisions/`](decisions/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`decisions/README.md`](decisions/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`governance/`](governance/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`governance/README.md`](governance/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
||||
|
||||
## Guardrails
|
||||
|
||||
These are load-bearing. Violating any means the design is wrong, not the rule.
|
||||
|
||||
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer. `xterm.dart` is the terminal renderer; markdown, canvas, graph are custom `CustomPaint`/widget components.
|
||||
- **Single process.** The Flutter app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), extensions. No separate daemon binary (D-56 dissolved it). The CLI surface for Claude is a thin C client (ptyc peer).
|
||||
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-1`](decisions/architecture.md#d-1-cli-first-not-mcp).
|
||||
- **Dart is the core; native supporter tools fill specific gaps.** `ptyc` (C) for PTY spawning. `pql` (Go) for queries. No second "core language." See [`D-5`](decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (amended by D-56).
|
||||
- **Own the rendering stack.** PTY (via `ptyc`), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages.
|
||||
- **User/Claude parity.** Every CLI subcommand has a UI affordance, and every UI action has a CLI. See [`D-6`](decisions/architecture.md#d-6-cli-and-event-surface-contract).
|
||||
- **pql: wrap, don't duplicate.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-3`](decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
||||
- **Single process.** The Flutter app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), extensions. No separate daemon binary (D-56 dissolved it).
|
||||
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-1`](governance/decisions/architecture.md#d-1-cli-first-not-mcp).
|
||||
- **Dart is the core; pql fills the query gap.** PTY spawning is native Dart FFI (`forkpty`). `pql` (Go) handles vault queries. No second "core language." See [`D-5`](governance/decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (amended by D-56).
|
||||
- **Own the rendering stack.** PTY (via Dart FFI), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages.
|
||||
- **User/Claude parity.** Every CLI subcommand has a UI affordance, and every UI action has a CLI. See [`D-6`](governance/decisions/architecture.md#d-6-cli-and-event-surface-contract).
|
||||
- **pql: wrap, don't duplicate.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-3`](governance/decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
||||
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept.
|
||||
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. See [`D-4`](decisions/architecture.md#d-4-ignore-file-strategy).
|
||||
- **Decision discipline.** All architectural choices live in `decisions/<domain>.md` as `D-NNN` records. Open questions as `Q-NNN`. Rejected alternatives as `R-NNN`. Claim new IDs via `pql decisions claim D <domain> "title"`. See [`decisions/README.md`](decisions/README.md).
|
||||
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. See [`D-4`](governance/decisions/architecture.md#d-4-ignore-file-strategy).
|
||||
- **Decision discipline.** All architectural choices live in `governance/decisions/<domain>.md` as `D-NNN` records. Open questions as `Q-NNN` under `governance/questions/<domain>.md`. Rejected alternatives as `R-NNN` under `governance/rejected/<domain>.md`. Claim new IDs via `pql decisions claim D <domain> "title"`. See [`governance/README.md`](governance/README.md).
|
||||
- **No pre-existing excuse.** Solo-dev repo — every failure encountered is yours to fix, regardless of who introduced it. If `make test` is red, a golden is broken, or `flutter analyze` shows a warning when you start working, the order is: **fix it first, then your work**. If you genuinely can't fix it in scope (separate ticket, large sweep, missing context), stop and surface it before continuing — don't push on top of broken state. "It was already broken" is not a reason to add more on top.
|
||||
|
||||
## Repo layout
|
||||
|
||||
@@ -45,9 +45,8 @@ lib/
|
||||
test/ # All tests (core subsystems + widgets + goldens + a11y)
|
||||
assets/ # Fonts, themes, grammars, licenses, logo
|
||||
linux/, macos/, web/ # Flutter platform directories
|
||||
ptyc/ # C PTY helper
|
||||
native/ # Vendored native libs (libtree-sitter.so)
|
||||
decisions/ # D/Q/R records
|
||||
native/ # Vendored native libs (libtree-sitter.so, dugite)
|
||||
governance/ # D/Q/R records (decisions/, questions/, rejected/ subdirs)
|
||||
docs/ # Design docs, wireframes
|
||||
legacy/ # Python Textual clide v1.2 (frozen)
|
||||
```
|
||||
@@ -55,8 +54,8 @@ legacy/ # Python Textual clide v1.2 (frozen)
|
||||
## Dependencies & supply chain
|
||||
|
||||
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
||||
- **Document every bundled dependency.** Listed in [`assets/licenses.yaml`](assets/licenses.yaml) with name, kind, version, homepage, license, and purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry. See [`D-42`](decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml).
|
||||
- **`ptyc` and any future native supporter tool:** no dep graph by design (libc-only for `ptyc`). "Audit" is reading the source before each bump.
|
||||
- **Document every bundled dependency.** Listed in [`assets/licenses.yaml`](assets/licenses.yaml) with name, kind, version, homepage, license, and purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry. See [`D-42`](governance/decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml).
|
||||
- **Native deps (dugite, libtree-sitter):** vendored in `native/`, pinned by SHA. Bumps follow the same advisory-review + `licenses.yaml` rule.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -70,7 +69,6 @@ make test-a11y # accessibility contract tests
|
||||
make test-integration# real app boot integration tests
|
||||
make build-linux # flutter build linux
|
||||
make build-macos # flutter build macos
|
||||
make ptyc-build # build the ptyc PTY-spawn helper
|
||||
make push-check # pre-push gate: decisions + core + fast tests + a11y
|
||||
make hooks # install the repo's git hooks (one-time setup)
|
||||
make clean # remove build artefacts
|
||||
@@ -84,4 +82,4 @@ One-time setup on a fresh clone: `make hooks && flutter pub get` once Flutter is
|
||||
|
||||
## Open questions
|
||||
|
||||
Open questions live under [`decisions/questions-*.md`](decisions/questions.md).
|
||||
Open questions live under [`governance/questions/`](governance/questions/).
|
||||
|
||||
@@ -102,9 +102,9 @@ test-e2e: ## End-to-end Playwright smoke.
|
||||
.PHONY: test-all
|
||||
test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequentially.
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: ## flutter test --coverage + lcov summary.
|
||||
ci/test_coverage.sh
|
||||
.PHONY: coverage-gate
|
||||
coverage-gate: ## Coverage gate — fails if total line % < pubspec.yaml `coverage_floor:` (D-66). Assumes `make test` ran first.
|
||||
ci/coverage_gate.sh
|
||||
|
||||
.PHONY: smoke-bundle
|
||||
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
|
||||
@@ -167,11 +167,11 @@ ifeq ($(FLUTTER_OS),linux)
|
||||
done
|
||||
@mkdir -p $(HOME)/.local/share/applications
|
||||
@sed 's|Exec=clide|Exec=$(INSTALL_PREFIX)/clide/clide|' linux/clide.desktop \
|
||||
> $(HOME)/.local/share/applications/clide.desktop
|
||||
> $(HOME)/.local/share/applications/net.schweitz.clide.desktop
|
||||
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||
@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
|
||||
@echo "installed: $(INSTALL_DIR)/clide -> $(INSTALL_PREFIX)/clide/clide"
|
||||
@echo "desktop: ~/.local/share/applications/clide.desktop"
|
||||
@echo "desktop: ~/.local/share/applications/net.schweitz.clide.desktop"
|
||||
@echo "version: $(VERSION)"
|
||||
else ifeq ($(FLUTTER_OS),macos)
|
||||
@mkdir -p $(HOME)/Applications
|
||||
@@ -189,6 +189,7 @@ uninstall: ## Remove installed clide.
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
rm -f $(INSTALL_DIR)/clide
|
||||
rm -rf $(INSTALL_PREFIX)/clide
|
||||
rm -f $(HOME)/.local/share/applications/net.schweitz.clide.desktop
|
||||
rm -f $(HOME)/.local/share/applications/clide.desktop
|
||||
@for size in $(ICON_SIZES); do \
|
||||
rm -f $(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps/clide.png; \
|
||||
@@ -233,49 +234,20 @@ dugite-fetch: ## Download and extract the dugite-native git distribution.
|
||||
dugite-clean: ## Remove the dugite-native directory.
|
||||
rm -rf $(DUGITE_DIR)
|
||||
|
||||
# -- ptyc (C supporter tool) ---------------------------------------------
|
||||
|
||||
PTYC_PRESENT := $(shell test -f ptyc/Makefile && echo yes || echo no)
|
||||
|
||||
.PHONY: ptyc-build
|
||||
ptyc-build: ## Build the ptyc PTY-spawn helper.
|
||||
ifeq ($(PTYC_PRESENT),yes)
|
||||
$(MAKE) -C ptyc
|
||||
else
|
||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
||||
endif
|
||||
|
||||
.PHONY: ptyc-test
|
||||
ptyc-test: ## Run ptyc smoke tests (SCM_RIGHTS round-trip).
|
||||
ifeq ($(PTYC_PRESENT),yes)
|
||||
$(MAKE) -C ptyc test
|
||||
else
|
||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
||||
endif
|
||||
|
||||
.PHONY: ptyc-clean
|
||||
ptyc-clean: ## Clean ptyc build artefacts.
|
||||
ifeq ($(PTYC_PRESENT),yes)
|
||||
$(MAKE) -C ptyc clean
|
||||
else
|
||||
@echo "(ptyc/ not scaffolded yet; skipping)"
|
||||
endif
|
||||
|
||||
# -- security -------------------------------------------------------------
|
||||
|
||||
.PHONY: security
|
||||
security: ## Dart advisory review + ptyc source review.
|
||||
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps;"
|
||||
@echo " ptyc is reviewed by reading it (tiny libc-only C)."
|
||||
security: ## Dart advisory review.
|
||||
@echo "security: Dart advisories reviewed manually before pubspec.yaml bumps."
|
||||
|
||||
# -- pre-push gate --------------------------------------------------------
|
||||
|
||||
.PHONY: decisions-validate
|
||||
decisions-validate: ## Parser dry-run over decisions/*.md.
|
||||
decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejected}/*.md.
|
||||
pql decisions validate
|
||||
|
||||
.PHONY: push-check
|
||||
push-check: decisions-validate test-core test test-a11y ## Pre-push gate.
|
||||
push-check: decisions-validate test-core test test-a11y coverage-gate ## Pre-push gate.
|
||||
|
||||
.PHONY: hooks
|
||||
hooks: ## Install the repo's git hooks.
|
||||
@@ -287,6 +259,5 @@ hooks: ## Install the repo's git hooks.
|
||||
.PHONY: clean
|
||||
clean: ## Remove build artefacts.
|
||||
rm -rf build .dart_tool
|
||||
$(MAKE) ptyc-clean
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# clide
|
||||
|
||||
A Flutter desktop IDE for Claude Code. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux and macOS.
|
||||
An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux and macOS.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -39,10 +39,10 @@ make push-check # pre-push gate: decisions + core + fast tests
|
||||
|
||||
## Status
|
||||
|
||||
Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](legacy/).
|
||||
Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy).
|
||||
|
||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Architectural decisions: [`decisions/`](decisions/).
|
||||
Design doc: [`docs/initial-plan.md`](https://github.com/postmeridiem/clide/blob/main/docs/initial-plan.md). Architectural decisions: [`decisions/`](https://github.com/postmeridiem/clide/tree/main/decisions).
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [`LICENSE`](LICENSE).
|
||||
MIT. See [`LICENSE`](https://github.com/postmeridiem/clide/blob/main/LICENSE).
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# clide tmux.conf — loaded via tmux -f for every Claude pane session.
|
||||
# Tuned for embedding inside xterm.dart; no status bar, large
|
||||
# scrollback, mouse-scroll passthrough, zero escape delay.
|
||||
|
||||
# No status bar — clide renders its own pane chrome.
|
||||
set -g status off
|
||||
|
||||
# 50k lines of scrollback (tmux default is 2000).
|
||||
set -g history-limit 50000
|
||||
|
||||
# Zero escape delay — xterm.dart delivers escape sequences
|
||||
# atomically, so the 500ms default just adds latency.
|
||||
set -sg escape-time 0
|
||||
|
||||
# Mouse on — scroll wheel events reach tmux's copy-mode so the
|
||||
# user can scroll back through Claude output.
|
||||
set -g mouse on
|
||||
|
||||
# 256color + true-color passthrough.
|
||||
set -g default-terminal "xterm-256color"
|
||||
set -ga terminal-overrides ",xterm-256color:Tc"
|
||||
|
||||
# Don't ring the bell visually or audibly — clide owns notifications.
|
||||
set -g visual-bell off
|
||||
set -g bell-action none
|
||||
|
||||
# Keep the session alive when the shell exits — clide manages
|
||||
# lifecycle via pane.close, not tmux session destruction.
|
||||
set -g remain-on-exit off
|
||||
|
||||
# Allow alt-screen passthrough for full-screen programs.
|
||||
set -g alternate-screen on
|
||||
|
||||
# Focus events let the terminal's focus tracking work through tmux.
|
||||
set -g focus-events on
|
||||
@@ -87,17 +87,18 @@ dependencies:
|
||||
YAML parser for theme files and extension manifests. Justified
|
||||
exception to prefer-zero-deps; Dart-team maintained.
|
||||
|
||||
- name: xterm
|
||||
kind: dart-package
|
||||
- name: terminal (based on xterm.dart)
|
||||
kind: inlined-source
|
||||
version: "4.0.0"
|
||||
homepage: https://pub.dev/packages/xterm
|
||||
homepage: https://github.com/TerminalStudio/xterm.dart
|
||||
license: MIT
|
||||
license_file: lib/src/terminal/LICENSE
|
||||
purpose: >-
|
||||
Flutter-native terminal emulator (ANSI / xterm / truecolor
|
||||
parser + renderer). Powers every pane that renders a PTY —
|
||||
general terminal, Claude, diff views running shell commands.
|
||||
Writing a vt100 / ANSI parser is weeks of work for no fidelity
|
||||
gain.
|
||||
Terminal emulator (ANSI / xterm / truecolor parser + renderer).
|
||||
Inlined from xterm.dart v4.0.0 by xuty (MIT) and modified:
|
||||
Scrollable removed, quiver dependency replaced, zmodem/debugger
|
||||
stripped, scroll forwarding rewritten. Original copyright and
|
||||
MIT license preserved in lib/src/terminal/LICENSE.
|
||||
|
||||
- name: ffi
|
||||
kind: dart-package
|
||||
|
||||
@@ -1,436 +0,0 @@
|
||||
// clide — CLI + daemon entry point.
|
||||
//
|
||||
// One binary, two modes (per D-005):
|
||||
// * `clide <subcommand>` — one-shot; connects to the daemon socket,
|
||||
// sends a request, prints the response, exits with the D-006
|
||||
// exit code.
|
||||
// * `clide --daemon` — long-running; owns the socket, dispatches
|
||||
// requests. Subsystems (pane, files, editor, …) register handlers
|
||||
// at boot.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/kernel/src/toolchain.dart';
|
||||
import 'package:clide/src/git/client.dart';
|
||||
// Daemon-only deep imports — these pull in dart:ffi (PTY) and
|
||||
// daemon-subsystem wiring that the Flutter app doesn't need and
|
||||
// can't compile for web. See lib/clide.dart for the barrel split.
|
||||
import 'package:clide/src/daemon/editor_commands.dart';
|
||||
import 'package:clide/src/daemon/files_commands.dart';
|
||||
import 'package:clide/src/daemon/git_commands.dart';
|
||||
import 'package:clide/src/daemon/pane_commands.dart';
|
||||
import 'package:clide/src/daemon/pql_commands.dart';
|
||||
import 'package:clide/src/editor/registry.dart' show EditorRegistry;
|
||||
import 'package:clide/src/panes/registry.dart';
|
||||
import 'package:clide/src/pql/client.dart';
|
||||
|
||||
Future<void> main(List<String> argv) async {
|
||||
if (argv.isEmpty) {
|
||||
_printHelp(stdout);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (argv.first == '--daemon') {
|
||||
await _runDaemon(argv.sublist(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Tier-2 single-word shortcuts (per CLAUDE.md). Each maps a flat
|
||||
// positional argv into the structured IPC shape of the canonical
|
||||
// editor.* / pane.* verb. Keeps Claude's tool-use pattern short.
|
||||
final rest = argv.sublist(1);
|
||||
switch (argv.first) {
|
||||
case '--version':
|
||||
case 'version':
|
||||
await _runCliArgs('version', const {}, exitOnOk: true);
|
||||
case '--help':
|
||||
case '-h':
|
||||
case 'help':
|
||||
_printHelp(stdout);
|
||||
exit(0);
|
||||
case 'ping':
|
||||
await _runCliArgs('ping', const {}, exitOnOk: true);
|
||||
case 'open':
|
||||
if (rest.isEmpty) _die('usage: clide open <path>');
|
||||
await _runCliArgs('editor.open', {'path': rest.first}, exitOnOk: true);
|
||||
case 'active':
|
||||
await _runCliArgs('editor.active', const {}, exitOnOk: true);
|
||||
case 'insert':
|
||||
final text = await _readTextArg(rest);
|
||||
await _runCliArgs('editor.insert', {'text': text}, exitOnOk: true);
|
||||
case 'replace-selection':
|
||||
final text = await _readTextArg(rest);
|
||||
await _runCliArgs(
|
||||
'editor.replace-selection',
|
||||
{'text': text},
|
||||
exitOnOk: true,
|
||||
);
|
||||
case 'save':
|
||||
await _runCliArgs('editor.save', const {}, exitOnOk: true);
|
||||
case 'git':
|
||||
await _runGit(rest);
|
||||
case 'tail':
|
||||
await _runTail(rest);
|
||||
default:
|
||||
// Unknown-to-the-CLI commands still go over IPC — the daemon is
|
||||
// authoritative about what's registered. Lets extensions add
|
||||
// subcommands without the CLI caring. Args forward as-is under
|
||||
// {argv: [...]} so daemon-side can parse whatever shape it wants.
|
||||
await _runCliArgs(
|
||||
argv.first,
|
||||
{'argv': rest},
|
||||
exitOnOk: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _printHelp(IOSink sink) {
|
||||
sink.writeln('''
|
||||
clide $clideVersion — Flutter desktop IDE for Claude Code.
|
||||
|
||||
Usage:
|
||||
clide --daemon Run the long-running daemon process.
|
||||
clide <subcommand> Run a one-shot subcommand against the daemon.
|
||||
|
||||
Built-in subcommands:
|
||||
ping Round-trip a ping to the daemon.
|
||||
version Print the clide version.
|
||||
help Print this help.
|
||||
|
||||
Editor (tier 2):
|
||||
open <path> Open a file in the editor (editor.open).
|
||||
active Print the active buffer (editor.active).
|
||||
insert <text|-> Insert text at the cursor in the active buffer.
|
||||
`-` reads text from stdin.
|
||||
replace-selection <…> Replace the selected text in the active buffer.
|
||||
`-` reads text from stdin.
|
||||
save Save the active buffer (editor.save).
|
||||
|
||||
Git (tier 3):
|
||||
git status Working tree status (staged/unstaged/conflicts).
|
||||
git diff [--staged] [P] Diff for file(s) P, or all if omitted.
|
||||
git stage <paths…> Stage files (git add).
|
||||
git stage-all Stage everything (git add -A).
|
||||
git unstage [paths…] Unstage files (git reset HEAD).
|
||||
git discard <paths…> Discard unstaged changes in files.
|
||||
git commit "<msg>" Commit staged changes.
|
||||
git log [--count N] Recent commit log (default 20).
|
||||
git stash [--message M] Stash working changes.
|
||||
git stash-pop Pop the top stash entry.
|
||||
git pull Pull from remote.
|
||||
git push [remote] [br] Push to remote.
|
||||
|
||||
Event subscription:
|
||||
tail --events [--filter SUBSYSTEM[:ID]]
|
||||
Stream events as JSON lines. --filter keeps
|
||||
only events from one subsystem, optionally
|
||||
narrowed to a single id. Exits on SIGINT.
|
||||
|
||||
Any other subcommand is forwarded to the daemon; registered handlers
|
||||
(e.g. `clide git status` once `builtin.git` lands) resolve there.
|
||||
Matches D-006's exit-code contract:
|
||||
0 success · 1 user-error · 2 tool-error · 3 not-found · 4 conflict
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _runDaemon(List<String> args) async {
|
||||
final socketPath = defaultSocketPath();
|
||||
final dispatcher = DaemonDispatcher();
|
||||
late final DaemonServer server;
|
||||
server = DaemonServer(
|
||||
socketPath: socketPath,
|
||||
dispatch: dispatcher.dispatch,
|
||||
);
|
||||
final toolchain = Toolchain();
|
||||
toolchain.applyResolved(Toolchain.resolvePaths(workspaceRoot: Directory.current.path));
|
||||
|
||||
final events = _ServerEventSink(server);
|
||||
final registry = PaneRegistry(events: events);
|
||||
registerPaneCommands(dispatcher, registry);
|
||||
|
||||
final files = FilesService.atCwd(events: events);
|
||||
registerFilesCommands(dispatcher, files);
|
||||
|
||||
final editor = EditorRegistry(events: events, workspaceRoot: files.root);
|
||||
registerEditorCommands(dispatcher, editor);
|
||||
final gitClient = GitClient(toolchain: toolchain, workDir: files.root);
|
||||
registerGitCommands(dispatcher, gitClient, events);
|
||||
|
||||
final pql = PqlClient(workDir: files.root, toolchain: toolchain);
|
||||
registerPqlCommands(dispatcher, pql);
|
||||
|
||||
final stopping = Completer<void>();
|
||||
void shutdown(ProcessSignal sig) {
|
||||
if (!stopping.isCompleted) {
|
||||
stderr.writeln('clide daemon: received ${sig.toString()}, shutting down');
|
||||
stopping.complete();
|
||||
}
|
||||
}
|
||||
|
||||
ProcessSignal.sigint.watch().listen(shutdown);
|
||||
ProcessSignal.sigterm.watch().listen(shutdown);
|
||||
|
||||
await server.start();
|
||||
await stopping.future;
|
||||
await registry.shutdown();
|
||||
await editor.shutdown();
|
||||
await files.shutdown();
|
||||
await server.stop();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/// Thin adapter: the server doesn't `implement DaemonEventSink` itself
|
||||
/// (that would tie ipc/ to panes/); instead the daemon entrypoint wraps
|
||||
/// it at the seam where both are known.
|
||||
class _ServerEventSink implements DaemonEventSink {
|
||||
_ServerEventSink(this._server);
|
||||
final DaemonServer _server;
|
||||
|
||||
@override
|
||||
void emit(IpcEvent event) => _server.broadcast(event);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Read the "text" argument for insert / replace-selection. A lone
|
||||
/// `-` means "slurp stdin"; anything else is concatenated into the
|
||||
/// text body (so `clide insert hello world` emits "hello world").
|
||||
Future<String> _readTextArg(List<String> rest) async {
|
||||
if (rest.isEmpty) _die('usage: clide <verb> <text> (or `-` to read stdin)');
|
||||
if (rest.length == 1 && rest.first == '-') {
|
||||
final bytes = <int>[];
|
||||
await for (final chunk in stdin) {
|
||||
bytes.addAll(chunk);
|
||||
}
|
||||
return utf8.decode(bytes);
|
||||
}
|
||||
return rest.join(' ');
|
||||
}
|
||||
|
||||
Future<Socket> _connectSocket() async {
|
||||
final socketPath = defaultSocketPath();
|
||||
try {
|
||||
return await Socket.connect(
|
||||
InternetAddress(socketPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
} catch (_) {
|
||||
_emitError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: 'daemon not reachable at $socketPath',
|
||||
hint: 'run `clide --daemon` in another terminal.',
|
||||
);
|
||||
exit(IpcExitCode.toolError);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runCliArgs(
|
||||
String cmd,
|
||||
Map<String, Object?> args, {
|
||||
required bool exitOnOk,
|
||||
}) async {
|
||||
final socket = await _connectSocket();
|
||||
final request = IpcRequest(id: '1', cmd: cmd, args: args);
|
||||
socket.writeln(request.encode());
|
||||
|
||||
// Responses come back on the same socket. Events may be interleaved
|
||||
// (the daemon broadcasts), so we skip events until we see the
|
||||
// response whose id matches our request.
|
||||
final lines = socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter());
|
||||
|
||||
try {
|
||||
await for (final line in lines) {
|
||||
if (line.isEmpty) continue;
|
||||
final msg = IpcMessage.decode(line);
|
||||
if (msg is! IpcResponse) continue;
|
||||
if (msg.id != request.id) continue;
|
||||
await socket.close();
|
||||
if (msg.ok) {
|
||||
stdout.writeln(jsonEncode(msg.data));
|
||||
if (exitOnOk) exit(IpcExitCode.ok);
|
||||
return;
|
||||
} else {
|
||||
final err = msg.error!;
|
||||
_emitError(
|
||||
code: err.code,
|
||||
kind: err.kind,
|
||||
message: err.message,
|
||||
hint: err.hint,
|
||||
);
|
||||
exit(err.code);
|
||||
}
|
||||
}
|
||||
_emitError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: 'daemon closed socket before responding',
|
||||
);
|
||||
exit(IpcExitCode.toolError);
|
||||
} on FormatException catch (e) {
|
||||
_emitError(
|
||||
code: IpcExitCode.toolError,
|
||||
kind: IpcErrorKind.toolError,
|
||||
message: 'bad response from daemon: $e',
|
||||
);
|
||||
exit(IpcExitCode.toolError);
|
||||
}
|
||||
}
|
||||
|
||||
/// `clide git <verb> [args]` — maps to `git.*` IPC verbs.
|
||||
Future<void> _runGit(List<String> args) async {
|
||||
if (args.isEmpty) {
|
||||
_die('usage: clide git <verb> [args…]');
|
||||
}
|
||||
switch (args.first) {
|
||||
case 'status':
|
||||
await _runCliArgs('git.status', const {}, exitOnOk: true);
|
||||
case 'diff':
|
||||
final staged = args.contains('--staged');
|
||||
final paths = args.sublist(1).where((a) => !a.startsWith('--')).toList();
|
||||
await _runCliArgs(
|
||||
'git.diff',
|
||||
{'staged': staged, if (paths.isNotEmpty) 'paths': paths},
|
||||
exitOnOk: true,
|
||||
);
|
||||
case 'stage':
|
||||
final paths = args.sublist(1);
|
||||
if (paths.isEmpty) _die('usage: clide git stage <path…>');
|
||||
await _runCliArgs('git.stage', {'paths': paths}, exitOnOk: true);
|
||||
case 'stage-all':
|
||||
await _runCliArgs('git.stage-all', const {}, exitOnOk: true);
|
||||
case 'unstage':
|
||||
final paths = args.sublist(1);
|
||||
await _runCliArgs('git.unstage', {'paths': paths}, exitOnOk: true);
|
||||
case 'discard':
|
||||
final paths = args.sublist(1);
|
||||
if (paths.isEmpty) _die('usage: clide git discard <path…>');
|
||||
await _runCliArgs('git.discard', {'paths': paths}, exitOnOk: true);
|
||||
case 'commit':
|
||||
if (args.length < 2) _die('usage: clide git commit "<message>"');
|
||||
final message = args.sublist(1).join(' ');
|
||||
await _runCliArgs('git.commit', {'message': message}, exitOnOk: true);
|
||||
case 'log':
|
||||
var count = 20;
|
||||
for (var i = 1; i < args.length; i++) {
|
||||
if (args[i] == '--count' && i + 1 < args.length) {
|
||||
count = int.tryParse(args[i + 1]) ?? 20;
|
||||
}
|
||||
}
|
||||
await _runCliArgs('git.log', {'count': count}, exitOnOk: true);
|
||||
case 'stash':
|
||||
String? message;
|
||||
for (var i = 1; i < args.length; i++) {
|
||||
if (args[i] == '--message' && i + 1 < args.length) {
|
||||
message = args[i + 1];
|
||||
}
|
||||
}
|
||||
await _runCliArgs(
|
||||
'git.stash',
|
||||
{if (message != null) 'message': message},
|
||||
exitOnOk: true,
|
||||
);
|
||||
case 'stash-pop':
|
||||
await _runCliArgs('git.stash-pop', const {}, exitOnOk: true);
|
||||
case 'pull':
|
||||
await _runCliArgs('git.pull', const {}, exitOnOk: true);
|
||||
case 'push':
|
||||
final rest = args.sublist(1);
|
||||
final setUpstream = rest.contains('-u');
|
||||
final positional = rest.where((a) => a != '-u').toList();
|
||||
await _runCliArgs(
|
||||
'git.push',
|
||||
{
|
||||
if (setUpstream) 'setUpstream': true,
|
||||
if (positional.isNotEmpty) 'remote': positional[0],
|
||||
if (positional.length > 1) 'branch': positional[1],
|
||||
},
|
||||
exitOnOk: true);
|
||||
default:
|
||||
_die('unknown git verb: ${args.first}');
|
||||
}
|
||||
}
|
||||
|
||||
/// `clide tail --events [--filter SUBSYSTEM[:ID]]` — stream events.
|
||||
Future<void> _runTail(List<String> args) async {
|
||||
// Parse flags: --events (required today; keeps us honest when more
|
||||
// modes like --history land), --filter SUBSYSTEM[:ID].
|
||||
var wantEvents = false;
|
||||
String? filterSubsystem;
|
||||
String? filterId;
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
final a = args[i];
|
||||
if (a == '--events') {
|
||||
wantEvents = true;
|
||||
} else if (a == '--filter') {
|
||||
if (i + 1 >= args.length) _die('--filter requires an argument');
|
||||
final spec = args[++i];
|
||||
final colon = spec.indexOf(':');
|
||||
if (colon < 0) {
|
||||
filterSubsystem = spec;
|
||||
} else {
|
||||
filterSubsystem = spec.substring(0, colon);
|
||||
filterId = spec.substring(colon + 1);
|
||||
}
|
||||
} else {
|
||||
_die('unknown argument: $a');
|
||||
}
|
||||
}
|
||||
if (!wantEvents) _die('clide tail: pass --events');
|
||||
|
||||
final socket = await _connectSocket();
|
||||
|
||||
// Shutdown on SIGINT / SIGTERM — close the socket so the stream
|
||||
// drains and we exit cleanly.
|
||||
void quit() {
|
||||
unawaited(socket.close());
|
||||
}
|
||||
|
||||
ProcessSignal.sigint.watch().listen((_) => quit());
|
||||
ProcessSignal.sigterm.watch().listen((_) => quit());
|
||||
|
||||
final lines = socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter());
|
||||
|
||||
try {
|
||||
await for (final line in lines) {
|
||||
if (line.isEmpty) continue;
|
||||
IpcMessage msg;
|
||||
try {
|
||||
msg = IpcMessage.decode(line);
|
||||
} on FormatException {
|
||||
continue;
|
||||
}
|
||||
if (msg is! IpcEvent) continue;
|
||||
if (filterSubsystem != null && msg.subsystem != filterSubsystem) continue;
|
||||
if (filterId != null && msg.data['id'] != filterId) continue;
|
||||
stdout.writeln(line);
|
||||
}
|
||||
} finally {
|
||||
await socket.close();
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void _emitError({
|
||||
required int code,
|
||||
required String kind,
|
||||
required String message,
|
||||
String? hint,
|
||||
}) {
|
||||
final err = IpcError(code: code, kind: kind, message: message, hint: hint);
|
||||
stderr.writeln(jsonEncode(err.toJson()));
|
||||
}
|
||||
|
||||
Never _die(String msg) {
|
||||
_emitError(
|
||||
code: IpcExitCode.userError,
|
||||
kind: IpcErrorKind.userError,
|
||||
message: msg,
|
||||
);
|
||||
exit(IpcExitCode.userError);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Coverage gate — fails if total line coverage drops below the
|
||||
# `coverage_floor:` value in pubspec.yaml. Driven by D-66.
|
||||
#
|
||||
# Reads coverage/lcov.info (generated by `flutter test --coverage`,
|
||||
# which `ci/test.sh` runs as part of the fast suite). Parses the
|
||||
# total LF/LH counts and compares the integer percentage against
|
||||
# the floor. The floor only ratchets up — bumping it requires an
|
||||
# explicit edit to pubspec.yaml committed alongside the test
|
||||
# additions that earned the bump.
|
||||
#
|
||||
# Self-contained parser (awk) — does not depend on `lcov` being
|
||||
# installed on the dev machine.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
LCOV=coverage/lcov.info
|
||||
|
||||
if [[ ! -f "$LCOV" ]]; then
|
||||
echo "==> coverage gate: $LCOV missing — run \`make test\` first (it writes lcov)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
floor=$(awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2; exit}' pubspec.yaml)
|
||||
if [[ -z "$floor" ]]; then
|
||||
echo "==> coverage gate: pubspec.yaml is missing coverage_floor: — see D-66" >&2
|
||||
exit 2
|
||||
fi
|
||||
read measured measured_int < <(
|
||||
awk -F: '
|
||||
/^LF:/ { lf += $2 }
|
||||
/^LH:/ { lh += $2 }
|
||||
END {
|
||||
pct = (lh / lf) * 100
|
||||
printf "%.2f %d\n", pct, int(pct)
|
||||
}
|
||||
' "$LCOV"
|
||||
)
|
||||
|
||||
if (( measured_int < floor )); then
|
||||
echo "==> coverage gate FAIL: ${measured}% < floor ${floor}%"
|
||||
echo " Add tests, or — if the drop is intentional — explain in the commit and lower the floor explicitly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (( measured_int > floor )); then
|
||||
echo "==> coverage gate OK: ${measured}% (floor ${floor}%) — ${measured_int}% available; consider bumping pubspec.yaml coverage_floor: to ${measured_int}"
|
||||
else
|
||||
echo "==> coverage gate OK: ${measured}% (floor ${floor}%)"
|
||||
fi
|
||||
@@ -14,5 +14,5 @@ dart format --set-exit-if-changed .
|
||||
echo "==> dart test (forkpty — incompatible with flutter test runner)"
|
||||
dart test --tags forkpty test/pty/session_test.dart
|
||||
|
||||
echo "==> flutter test (unit + widget + golden)"
|
||||
flutter test --exclude-tags forkpty
|
||||
echo "==> flutter test --coverage (unit + widget + golden)"
|
||||
flutter test --coverage --exclude-tags forkpty
|
||||
|
||||
@@ -20,11 +20,6 @@ if ! command -v dart >/dev/null; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v ptyc >/dev/null && [[ ! -x "ptyc/bin/ptyc" ]]; then
|
||||
echo "test-core: building ptyc (required by PTY tests)"
|
||||
make -C ptyc >/dev/null
|
||||
fi
|
||||
|
||||
# Hard timeout (seconds). The PTY tests should finish in <5s; IPC/daemon
|
||||
# tests are faster still. 120s is generous for CI warmup, tiny for a
|
||||
# hang.
|
||||
@@ -33,7 +28,7 @@ TIMEOUT_SECONDS=${TIMEOUT_SECONDS:-120}
|
||||
# Run dart test in its own process group so we can kill descendants on
|
||||
# timeout. `setsid` starts a new session; `timeout --kill-after` SIGKILLs
|
||||
# after SIGTERM if the test ignores it.
|
||||
CORE_DIRS="test/ipc test/pty test/daemon test/git test/panes test/files test/editor test/cli test/pql"
|
||||
CORE_DIRS="test/ipc test/pty test/daemon test/git test/panes test/files test/editor test/pql"
|
||||
|
||||
echo "test-core: dart test ${CORE_DIRS} (timeout ${TIMEOUT_SECONDS}s)"
|
||||
if ! timeout --kill-after=5s "${TIMEOUT_SECONDS}s" \
|
||||
@@ -42,7 +37,6 @@ if ! timeout --kill-after=5s "${TIMEOUT_SECONDS}s" \
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
echo "test-core: TIMEOUT — killing descendants" >&2
|
||||
pkill -9 -f "dart test test/" 2>/dev/null || true
|
||||
pkill -9 -f "ptyc" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
exit $rc
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate + summarize lcov coverage. No thresholds yet (see plan's
|
||||
# "Open questions deferred" — we let the suite run for a week of real
|
||||
# commits before setting hard gates that would just need tuning).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> flutter test --coverage"
|
||||
flutter test --coverage
|
||||
|
||||
if command -v lcov >/dev/null 2>&1; then
|
||||
echo "==> lcov summary"
|
||||
lcov --summary coverage/lcov.info
|
||||
fi
|
||||
@@ -1,16 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# End-to-end layer: daemon subprocess + web WASM Playwright smoke.
|
||||
# Neither fits in `make test`; together they're the "everything still
|
||||
# works across process/runtime boundaries" gate.
|
||||
# End-to-end layer: web WASM Playwright smoke.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> build bin/clide (required by daemon subprocess test)"
|
||||
make build
|
||||
|
||||
echo "==> daemon subprocess test"
|
||||
dart test test/daemon/
|
||||
|
||||
echo "==> browser WASM smoke (Playwright)"
|
||||
./tools/ui/build.sh
|
||||
./tools/ui/serve.sh
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# Decisions
|
||||
|
||||
Confirmed decisions, open questions, and rejected alternatives for clide.
|
||||
|
||||
Decisions are split by domain. When unsure where a record belongs: if
|
||||
it constrains **how we build**, it's architecture. If it defines **what
|
||||
ships to users**, it's extensions / accessibility. If it defines **how
|
||||
we verify**, it's testing. If it defines **what the toolchain looks
|
||||
like**, it's tooling. If it defines **how the team works**, it's
|
||||
process.
|
||||
|
||||
Cross-domain records live in one file with `[D-NNN]`-shaped cross-
|
||||
references in related files. Split threshold: when any file exceeds
|
||||
~350 lines, review whether it should split (see settled-reach's
|
||||
`questions-*.md` split pattern for precedent).
|
||||
|
||||
## Domain files
|
||||
|
||||
| File | Domain |
|
||||
|------|--------|
|
||||
| [architecture.md](architecture.md) | Core, rendering, IPC, kernel, panel manager |
|
||||
| [extensions.md](extensions.md) | Extension contract, Lua runtime, grain, contribution points |
|
||||
| [accessibility.md](accessibility.md) | A11y + i18n policy, WCAG gates |
|
||||
| [testing.md](testing.md) | Test pyramid, drivers, client-side constraint |
|
||||
| [tooling.md](tooling.md) | Toolchain, supply chain, CI, ignore strategy |
|
||||
| [process.md](process.md) | Q&D system, kanban, commit conventions, changelog |
|
||||
| [rejected.md](rejected.md) | Rejected alternatives across all domains |
|
||||
| [questions.md](questions.md) | Master index of open questions |
|
||||
| [questions-architecture.md](questions-architecture.md) | Architecture Qs |
|
||||
| [questions-extensions.md](questions-extensions.md) | Extension Qs |
|
||||
| [questions-accessibility.md](questions-accessibility.md) | A11y / i18n Qs |
|
||||
| [questions-testing.md](questions-testing.md) | Testing Qs |
|
||||
| [questions-process.md](questions-process.md) | Process + tooling Qs |
|
||||
|
||||
## Record shape
|
||||
|
||||
Confirmed decisions (`D-NNN`):
|
||||
|
||||
```markdown
|
||||
### D-NNN: Short title
|
||||
- **Date:** YYYY-MM-DD
|
||||
- **Decision:** one-sentence summary, then details.
|
||||
- **Rationale:** why this over alternatives.
|
||||
- **Cost:** known downsides / what we're accepting.
|
||||
- **Raised by:** who proposed / endorsed.
|
||||
```
|
||||
|
||||
Domain-specific fields (`Kill switch:`, `Evaluation reports:`,
|
||||
`Amendment:`, `Cross-reference:`) are additive. Amendments are inline
|
||||
and dated: `**Amendment (YYYY-MM-DD):** …`. Cross-references use
|
||||
markdown anchor links with the full slug:
|
||||
`[D-5](architecture.md#d-5-dart-core-ptyc-peer)`.
|
||||
|
||||
Open questions (`Q-NNN`):
|
||||
|
||||
```markdown
|
||||
### Q-NNN: Short question-form title
|
||||
- **Status:** Open | Partially resolved → [D-NNN] | Resolved → [D-NNN]
|
||||
- **Question:** ...
|
||||
- **Context:** ...
|
||||
- **Assigned to:** (optional)
|
||||
- **Source:** (optional)
|
||||
```
|
||||
|
||||
Rejected alternatives (`R-NNN`):
|
||||
|
||||
```markdown
|
||||
### R-NNN: Short rejected-option title
|
||||
- **Rejected:** YYYY-MM-DD
|
||||
- **Reason:** ...
|
||||
- **Cross-reference:** [D-NNN] (what was picked instead)
|
||||
```
|
||||
|
||||
## Claiming an ID
|
||||
|
||||
Until the pql planning subcommands land ([`Q-21`](questions-process.md)),
|
||||
claim IDs by inspecting the highest existing `D-NNN` / `Q-NNN` /
|
||||
`R-NNN` in the target file and incrementing.
|
||||
|
||||
Once `pql decisions claim D <domain> "title"` exists, use that —
|
||||
same semantics, no race on concurrent sessions.
|
||||
|
||||
## Querying
|
||||
|
||||
`pql decisions …` reads `decisions/*.md` and writes `.pql/pql.db`
|
||||
(gitignored; markdown is the source of truth).
|
||||
|
||||
Common queries:
|
||||
|
||||
```bash
|
||||
pql decisions list --type confirmed --domain architecture
|
||||
pql decisions show D-5 --with-refs
|
||||
pql decisions coverage # D-records without tickets
|
||||
pql decisions validate # pre-push parser gate
|
||||
pql ticket board # kanban view of tickets
|
||||
```
|
||||
|
||||
## Adding a decision
|
||||
|
||||
1. Edit the appropriate domain file.
|
||||
2. Follow the record shape above.
|
||||
3. Run `pql decisions validate` (also runs in `make push-check`).
|
||||
4. Commit. The SQLite index rebuilds from markdown on any
|
||||
`pql decisions sync`.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Open Questions — Master Index
|
||||
|
||||
Open questions live in per-domain `questions-<domain>.md` files.
|
||||
This index is a pointer and a place to record the most-load-bearing
|
||||
open questions with one-line summaries.
|
||||
|
||||
## By domain
|
||||
|
||||
| File | Topics |
|
||||
|------|--------|
|
||||
| [questions-architecture.md](questions-architecture.md) | IPC, events, canvas, window chrome, macOS signing, pql absorption, ticket persistence, interaction model |
|
||||
| [questions-extensions.md](questions-extensions.md) | Extension API shape, Lua runtime vendoring, manifest schema version |
|
||||
| [questions-accessibility.md](questions-accessibility.md) | Web-mode a11y, i18n plurals/gender/dates |
|
||||
| [questions-testing.md](questions-testing.md) | Coverage gates, screen-reader automation |
|
||||
| [questions-process.md](questions-process.md) | Editor tab (LSP vs tree-sitter), icon set, theme hot-reload, kernel DB access, planning-tool location |
|
||||
|
||||
## Load-bearing questions (gate other work)
|
||||
|
||||
- **[Q-21](questions-process.md#q-21-pql-absorbs-planning-vs-keeps-separate)** — Pql absorbs planning features vs clide absorbs pql vs separate CLI. Blocks the stopgap sunset and shapes the pql-side planning session.
|
||||
- **[Q-22](questions-process.md#q-22-ticket-persistence-strategy)** — Ticket persistence: per-dev only / milestone-committed / markdown-mirrored. Shapes multi-contributor story.
|
||||
- **[Q-5](questions-architecture.md#q-5-ipc-wire-format-stability)** — IPC wire-format stability and `schema_version:` in `pubspec.yaml`. Decide when the first real subcommand lands.
|
||||
- **[Q-15](questions-process.md#q-15-editor-tab-full-lsp-vs-tree-sitter-only)** — Editor tab: full LSP integration vs tree-sitter-only highlight. Decide during Tier 2.
|
||||
|
||||
---
|
||||
@@ -1,63 +0,0 @@
|
||||
# Rejected Alternatives
|
||||
|
||||
Alternatives considered and rejected, with rationale preserved for
|
||||
future reference.
|
||||
|
||||
---
|
||||
|
||||
### R-2: Go sidecar
|
||||
- **Rejected:** 2026-04-20 (was ADR 0002; superseded by [D-5](architecture.md#d-5-dart-core-ptyc-peer))
|
||||
- **Reason:** The ADR picked Go on two premises — (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so muscle memory transfers. Both broke on reassessment. The sidecar stripped of PTY is I/O-bound glue that `dart:io` covers cleanly (unix sockets, JSON-lines framing, process tables, shell-outs). The real axis was *separate process vs shared language*, not Go vs Rust, and separate-process is what matters (session persistence needs the daemon to outlive the app), not language. PTY is the one place Dart is genuinely weak — Dart's multi-threaded VM can't safely `fork()` — and that single constraint forces a native helper regardless, independent of whether the rest of the core is Dart. Once a small native helper is accepted, the question "does *everything else* need to be in that same native language" answers itself: no. Go sidecar directory dissolved; `ptyc` (C, PTY-only, pql-peer) is the surviving native supporter tool.
|
||||
- **Cross-reference:** [D-5](architecture.md#d-5-dart-core-ptyc-peer)
|
||||
|
||||
### R-3: `MaterialApp` root
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Dragged in Material theming, default icons, and platform chrome that fought the custom three-tier theme pipeline ([D-9](architecture.md#d-9-three-tier-theme-pipeline)). Every bundled theme had to override Material defaults to look like clide; the overrides were visible in widget tests as "why is this `ElevatedButton` colored this way."
|
||||
- **Cross-reference:** [D-7](architecture.md#d-7-app-root-is-bare-widgetsapp)
|
||||
|
||||
### R-4: Flutter `intl` + ARB codegen for i18n
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** ARB codegen is inflexible for plugin-contributed catalogs — every catalogue needs a codegen pass, every extension ships with pre-generated Dart, and runtime merging is fighting the tool. The fframe text-driven pattern reads JSON at runtime with no codegen, which fits extension-shipped catalogs cleanly.
|
||||
- **Cross-reference:** [D-21](accessibility.md#d-21-i18n-is-a-tier-0-contract)
|
||||
|
||||
### R-5: Patrol test runner
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Adds a dependency (violates [D-31](tooling.md#d-31-prefer-zero-deps-exact-pin)) for a capability we get from Playwright + Flutter's own semantics tree. Patrol's value proposition (native-gesture emulation) is less relevant on Linux desktop than on mobile.
|
||||
- **Cross-reference:** [D-26](testing.md#d-26-web-driver-raw-playwright-plus-flutter-semantics)
|
||||
|
||||
### R-6: Nerd-font glyph icons
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** TUI hangover from the Python-era clide under `legacy/`. Not desktop-native; forces a font dependency; doesn't theme consistently. Clide uses custom icon primitives (Tier 6 revisits with proper icon-set design).
|
||||
- **Cross-reference:** [Q-17](questions-process.md#q-17-icon-set-growth)
|
||||
|
||||
### R-7: `CupertinoApp` root
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** iOS-opinionated; wrong shell for a Linux-primary desktop IDE. Same theming-collision problem as [R-3](#r-3-materialapp-root).
|
||||
- **Cross-reference:** [D-7](architecture.md#d-7-app-root-is-bare-widgetsapp)
|
||||
|
||||
### R-8: Riverpod / Provider / BLoC for state
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Violates [D-31](tooling.md#d-31-prefer-zero-deps-exact-pin). `ChangeNotifier` + `ListenableBuilder` ship in the SDK, fake trivially, and cover the state model we need. The ergonomic wins of Riverpod / Provider don't clear the "new dependency" bar at clide's scale.
|
||||
- **Cross-reference:** [D-10](architecture.md#d-10-state-management-changenotifier)
|
||||
|
||||
### R-9: Port planning tooling into clide
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Earlier in the planning session the assumption was "clide owns Dart subcommands for decisions + tickets." That breaks the day a contributor works in a terminal or in VS Code / JetBrains — they have no `clide` binary to run. Reversing: pql owns planning long-term (see [D-39](process.md#d-39-planning-tooling-lives-in-pql)); clide consumes via shell-out.
|
||||
- **Cross-reference:** [D-39](process.md#d-39-planning-tooling-lives-in-pql)
|
||||
|
||||
### R-10: Python-script stopgap under `tooling/db/`
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Location, not language. Settled-reach puts scripts at `tooling/db/` — copying that path here creates a script-pollution problem: every project using the pattern commits its own copy. The accepted Python port ([D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)) lives at `tools/scripts/plan`, clearly signalled as dev-tooling and time-limited.
|
||||
- **Cross-reference:** [D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||
|
||||
### R-11: Permanent stopgap
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** If the Python port under `tools/scripts/plan` outlasts pql's feature parity, delete it. The deletion commit should be one changeset: remove `tools/scripts/plan`, remove its Makefile target (`decisions-validate` rewires to `pql decisions validate`), add a `CHANGELOG.md` entry under Removed, and verify `.pql/pql.db` still opens under the new `pql` binary.
|
||||
- **Cross-reference:** [D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||
|
||||
### R-12: MaterialApp wrapper from design handoff
|
||||
- **Rejected:** 2026-04-22
|
||||
- **Reason:** The design handoff delivers theme files as `MaterialApp`/`ThemeData` Dart classes. This is the delivery format of claude.ai/design, not a design intent. Adopting Material's widget system would contradict [D-7](architecture.md#d-7-app-root-is-bare-widgetsapp) (bare WidgetsApp, no Material/Cupertino). We translate the palette tokens and syntax roles into our existing YAML + `SurfaceTokens` pipeline.
|
||||
- **Cross-reference:** [D-43](architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
||||
|
||||
---
|
||||
@@ -1,54 +0,0 @@
|
||||
# Tooling Decisions
|
||||
|
||||
Toolchain, supply chain, CI, ignore strategy.
|
||||
|
||||
---
|
||||
|
||||
### D-31: Prefer-zero-deps, exact-pin
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Default to writing code ourselves. Every third-party Dart dependency needs a paragraph of justification in the PR that adds it. What stays is exact-pinned in `pubspec.yaml` (no caret ranges), `pubspec.lock` is committed, and advisories are reviewed before every bump.
|
||||
- **Rationale:** Supply-chain gate. Flutter SDK + Dart SDK give us most of what we need; the dependencies we keep are the ones we can't reasonably write (yaml parser, mocktail, alchemist). Exact-pin because caret ranges mean "the CVE bumps itself in silently."
|
||||
- **Cost:** Longer PR descriptions for deps; occasional reinvention of a convenience. Accepted.
|
||||
- **Raised by:** 2026-04-21 planning; reinforced by user feedback memory.
|
||||
|
||||
### D-32: CI — Gitea primary, Linux-only runners, not yet activated
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** CI config lives at `.gitea/workflows/test.yml` (Gitea Actions consumes GitHub-Actions syntax). Runners are Linux only; macOS is tested locally. The workflow is ready but Gitea Actions is not yet activated on the instance — the file is a staged pipeline for review. If the repo moves to GitHub, the file copies to `.github/workflows/test.yml` verbatim.
|
||||
- **Rationale:** We want the CI story defined before we turn CI on — lower blast radius on early red builds. GitHub portability is free because the syntax is shared.
|
||||
- **Cost:** PRs don't run CI yet; `make push-check` is the gate until activation.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
|
||||
### D-42: Dependencies documented in `licenses.yaml`
|
||||
- **Date:** 2026-04-22
|
||||
- **Decision:** `app/assets/licenses.yaml` has three sections: `self:` (clide's MIT license, rendered first in the About screen so the user knows what they're running), `dependencies:` (third-party artefacts that **ship in the binary** — fonts, runtime Dart packages, native supporter tools, bundled data), and `dev_dependencies:` (build-time-only tooling — test runners, mocks, lints, golden harness — tracked for audit but **not rendered** in the About screen because they don't reach the user). Each entry has name, kind, version, homepage, license identifier, and a one-line purpose; runtime entries also carry a `license_file:` pointer to the bundled license text so the About screen can display it verbatim. Adding any dependency is a two-step commit: add the artefact **and** the corresponding `licenses.yaml` entry in the same changeset, under the correct section.
|
||||
- **Rationale:** Complements [D-31](#d-31-prefer-zero-deps-exact-pin). Prefer-zero-deps is a *budget*; `licenses.yaml` is the *visible consequence*. An extra row in the About screen is a review-time signal that the shipped-binary surface grew. Splitting dev deps out keeps the user-facing list small and honest — a test framework is not something the user needs to see in About — while still documenting every supply-chain input for audit completeness. The runtime entries discharge the redistribution obligations bundled licenses impose (OFL, MIT, BSD all require preserving the license text alongside the binary) without ad-hoc NOTICE files.
|
||||
- **Cost:** One extra edit per dep. Zero tolerance for drift — an un-listed dep is a contributor-visible bug. Until the About screen lands at Tier 6, `licenses.yaml` is accurate but not rendered; the discipline applies from now regardless so Tier 6 inherits a clean list.
|
||||
- **Raised by:** 2026-04-22 planning (user-directed best practice).
|
||||
|
||||
### D-33: Golden-output ignore pattern — `coverage.*` excludes output, not scripts
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** `.gitignore` excludes `coverage.*` (the lcov output files from `flutter test --coverage`). Coverage-related scripts are named `ci/test_coverage.sh` (not `ci/coverage.sh`) to stay outside the pattern.
|
||||
- **Rationale:** An earlier draft named the script `ci/coverage.sh` and it was silently git-ignored. Renaming the script is cheaper than narrowing the gitignore pattern (which risks re-introducing output churn).
|
||||
- **Cost:** Script names have a convention to follow.
|
||||
- **Raised by:** 2026-04-21 planning (caught during commit rehearsal).
|
||||
|
||||
### D-58: Format engines are adoptable dependencies
|
||||
- **Date:** 2026-04-23
|
||||
- **Decision:** The "own the rendering stack" guardrail applies to **UI chrome** — panels, tabs, panes, canvas, terminal, layout primitives. **Format engines** — packages that parse or render external file formats (SVG, markdown, HTML, terminal escape sequences, tree-sitter grammars) — are adoptable like any other dependency: vet, exact-pin, CVE-lock, document in `licenses.yaml`. They are not shortcuts for lazy coding; they are well-maintained renderers for formats we didn't invent. The distinction: if it renders *our* UI, we own it; if it renders *someone else's file format*, we adopt a parser/renderer and sandbox it.
|
||||
- **Adopted under this rule:** `jovial_svg` (SVG renderer), `markdown` (MD parser; renderer is ours), `flutter_widget_from_html_core` (HTML renderer; sandboxed), `xterm` (terminal emulator), tree-sitter (syntax highlighting). Canvas (`CustomPaint` + `InteractiveViewer`) stays in-house — UI chrome, not a format engine.
|
||||
- **Amendment to D-31 (prefer-zero-deps):** D-31's "prefer-zero-deps" still applies — every new dependency needs justification. This record clarifies that format engines clear the justification bar by default. The supply-chain gate (exact-pin, advisory review, `licenses.yaml`) still applies.
|
||||
- **Rationale:** Reimplementing SVG, markdown, or VT100 parsing adds months of work for no fidelity gain. tree-sitter already set this precedent. The key is sandboxing: HTML rendering must whitelist tags/attributes; SVG must not execute scripts; markdown rendering goes through our own widget builder so we control the output.
|
||||
- **Cost:** Each adopted engine adds transitive dependencies and supply-chain surface. Mitigated by exact-pinning and `make security`.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml).
|
||||
- **Raised by:** 2026-04-23 format engine evaluation.
|
||||
|
||||
### D-59: Bundled git via dugite-native
|
||||
- **Date:** 2026-04-25
|
||||
- **Decision:** Ship a self-contained Git binary from [dugite-native](https://github.com/desktop/dugite-native) (the same distribution GitHub Desktop bundles). Downloaded at build time via `make dugite-fetch`, stored under `native/dugite/`, gitignored. The `Toolchain` class resolves to the bundled binary first, falling back to system git on PATH.
|
||||
- **Rationale:** The macOS app sandbox blocks execution of Homebrew-installed git (symlinks resolve to Cellar paths that SBPL cannot match without freezing rendering). `/usr/bin/git` is an xcrun shim that refuses to run inside a sandbox. Bundling dugite-native makes clide self-contained — no dependency on Homebrew, Xcode CLT, or system git. The approach is proven: GitHub Desktop, Tower, and other git GUI apps all bundle their own git for the same reason.
|
||||
- **Alternatives rejected:** (R) libgit2 via FFI — missing porcelain commands (pull/push/rebase), no hooks, would require rewriting GitClient. (R) Build git from source — dugite-native already does this with better infra. (R) SBPL exceptions for Homebrew — `(subpath "/opt/homebrew")` for process-exec freezes Flutter rendering on macOS 26.
|
||||
- **Cost:** ~57 MB download (~199 MB unpacked, stripped at build time). Must track dugite-native releases for security updates. GPL-2.0 (git binary) applies to the bundled artefact, not to clide's MIT code.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml).
|
||||
- **Raised by:** 2026-04-25 macOS sandbox investigation.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,155 @@
|
||||
# PTY + IPC error-handling audit
|
||||
|
||||
Date: 2026-05-05
|
||||
Ticket: T-18
|
||||
Decision ref: D-5
|
||||
|
||||
Punch list of error-handling issues in `lib/src/pty/`, `lib/src/ipc/`,
|
||||
and `lib/src/daemon/`. Severity-ranked. Each item references the
|
||||
follow-up ticket where the fix lands.
|
||||
|
||||
## Critical — silent failures, leaks, races
|
||||
|
||||
1. **`lib/src/pty/native_pty.dart:155-158`** — `forkpty()` failure
|
||||
throws `StateError('forkpty() failed')` with no errno. Caller
|
||||
can't distinguish ENOMEM/EAGAIN/ENOENT-of-/dev/ptmx. Capture
|
||||
errno before `_freeAll` (which may trample it) and surface via
|
||||
`PtyException`. → T-75
|
||||
|
||||
2. **`lib/src/pty/native_pty.dart:160-165`** — Child process: `chdir`
|
||||
and `execve` returns are ignored. If `execve` returns (i.e.
|
||||
fails), we fall through to `_exit(1)` with no diagnostic. Write
|
||||
a one-line error envelope to fd 1 before exiting so the parent's
|
||||
reader sees "exec failed: ENOENT" instead of immediate EOF. → T-75
|
||||
|
||||
3. **`lib/src/pty/native_pty.dart:244-251`** — `write()` ignores
|
||||
`_nativeWrite` return. Short writes silently drop bytes; -1/EPIPE
|
||||
reported as successful "wrote -1". Loop until full length is
|
||||
written or surface errno on negative returns. → T-75
|
||||
|
||||
4. **`lib/src/pty/native_pty.dart:259-262`** — `resize()` ignores
|
||||
`_ioctl` and `_nativeKill` return values. EBADF on a half-closed
|
||||
fd silently no-ops. Set `_dead = true` on EBADF. → T-75
|
||||
|
||||
5. **`lib/src/pty/native_pty.dart:198-210`** — Race: `_spawnReader`
|
||||
is `async` but `NativePty.start` returns immediately. `close()`
|
||||
racing with isolate spawn can leave the isolate orphaned. Make
|
||||
`start` await reader spawn or track the spawn-future. → T-76
|
||||
|
||||
6. **`lib/src/pty/native_pty.dart:280-290`** — `close()` sets
|
||||
`_dead = true` *before* `_nativeClose(_fd)`, but the reader
|
||||
isolate continues polling on `_fd`. If a new fd reuses that
|
||||
number, the reader's `poll` may briefly target the wrong file.
|
||||
Send shutdown signal via SendPort or self-pipe before closing. → T-76
|
||||
|
||||
7. **`lib/src/pty/session.dart:135-153`** — Resource leak: if
|
||||
`_recvFdAsync`, `setWinsize`, `proc.stdout.first.timeout`, or
|
||||
`_extractPid` throws, the spawned ptyc Process and (in some
|
||||
cases) the received `masterFd` leak. Only line 151 closes
|
||||
`masterFd`. Wrap post-spawn block in try/catch that kills `proc`,
|
||||
closes `masterFd`, and rethrows. → T-76
|
||||
|
||||
8. **`lib/src/pty/session.dart:240`** — `_recvFdAsync`: if
|
||||
`Isolate.spawn` itself throws, `port` is leaked. Wrap in
|
||||
try/catch. → T-76
|
||||
|
||||
9. **`lib/src/pty/session.dart:165-176`** — `write()` returns raw
|
||||
`libc.write` result without checking < 0 / errno or looping for
|
||||
short writes. Same as #3. → T-75
|
||||
|
||||
10. **`lib/src/pty/session.dart:271-275`** — `Isolate.spawn(...).then(...)`
|
||||
is fire-and-forget. If spawn fails, the error is silently
|
||||
swallowed and `_readerIsolate` remains null forever. Add
|
||||
`.catchError` or await it. → T-76
|
||||
|
||||
11. **`lib/src/ipc/server.dart:30-39`** — `broadcast()` `try/catch (_)`
|
||||
swallows write errors with no logging. At least log the kind. → T-77
|
||||
|
||||
12. **`lib/src/ipc/server.dart:107`** — `client.writeln(resp.encode())`
|
||||
is not awaited and not guarded. If client disconnected mid-dispatch,
|
||||
this throws asynchronously with no `onError` handler. Wrap in
|
||||
try/catch and remove the client from `_clients`. → T-77
|
||||
|
||||
13. **`lib/src/ipc/server.dart:83-108`** — `_handleLine` runs
|
||||
`await dispatch(msg)` with no per-request timeout. A misbehaving
|
||||
handler blocks the connection's read pipeline indefinitely. → T-77
|
||||
|
||||
14. **`lib/src/ipc/server.dart:46-50`** — Stale-socket retry deletes
|
||||
the socket file unconditionally on `SocketException`. If two
|
||||
daemon instances race to start, the second rips the first's live
|
||||
socket out from under it. Try `connect()` first; refuse if a
|
||||
live daemon answers. → T-77
|
||||
|
||||
## High — degraded UX / debugging
|
||||
|
||||
15. **`lib/src/daemon/pane_commands.dart:87-96`** — `_spawn`
|
||||
catch-all flattens every failure into `tool_error: pane.spawn
|
||||
failed: <toString>`. "binary not found", "permission denied",
|
||||
"out of pty fds" all look the same. Map `PtyException.errno`
|
||||
(ENOENT/EACCES/EMFILE) to distinct hints/codes. → T-79
|
||||
|
||||
16. **`lib/src/daemon/editor_commands.dart:67-76`** — Same pattern;
|
||||
`editor.open` catch-all loses FileSystemException distinctions
|
||||
(ENOENT vs EACCES vs EISDIR). → T-79
|
||||
|
||||
17. **`lib/src/daemon/files_commands.dart:78`** — `file.readAsStringSync()`
|
||||
is unguarded; UTF-8 errors, permission errors, races with deletion
|
||||
turn into a 500-style dispatch error instead of a clean
|
||||
`IpcResponse.err`. Wrap in try/catch. → T-81
|
||||
|
||||
18. **`lib/src/daemon/files_commands.dart:74`** — Path is concatenated
|
||||
with `/` and never validated. `path: "../../../etc/passwd"`
|
||||
traverses out of `files.root`. Resolve and verify the resulting
|
||||
path stays under `root.absolute.path`. → T-78 (security)
|
||||
|
||||
19. **`lib/src/pty/session.dart:201-234`** — `close()` distinguishes
|
||||
EOF/EBADF/EIO only in comments. The 500ms timeout is silent
|
||||
(`onTimeout: () {}`). Log the timeout so we know when SIGKILL
|
||||
was actually needed. → T-81
|
||||
|
||||
20. **`lib/src/pty/session.dart:390-394`** — Reader isolate treats
|
||||
any negative read return that isn't EINTR as EOF — including
|
||||
transient EAGAIN or recoverable EIO. Inspect errno and log
|
||||
non-EBADF/EIO/0 cases. → T-81
|
||||
|
||||
21. **`lib/src/pty/ffi/scm_rights.dart:115-116`** — Returned cmsg-data
|
||||
fd is read without sanity-checking against `msgControllen`. A
|
||||
malformed peer that sends only a partial cmsg could let us read
|
||||
garbage as an fd. Verify `dataOffset + 4 <= msgControllen`
|
||||
before deref. → T-81
|
||||
|
||||
22. **`lib/src/ipc/server.dart:41-56`** — `start()` logs to
|
||||
`stderr.writeln` but the rest of the daemon uses no logger. In
|
||||
the Flutter-host process stderr is often consumed by the engine.
|
||||
Standardize on a logger. → T-80
|
||||
|
||||
## Medium — cleanliness
|
||||
|
||||
23. **`lib/src/pty/session.dart:390`, `native_pty.dart:262, 285`** —
|
||||
Magic errno/signal numbers (`4=EINTR`, `9=SIGKILL`, `28=SIGWINCH`,
|
||||
`_kSighup=1`). Pull into named constants. → T-80
|
||||
|
||||
24. **`lib/src/pty/ffi/libc.dart:232-245`** — `errno` getter does a
|
||||
`lookupFunction` on every access (catching ArgumentError every
|
||||
call on macOS). Cache the resolved function pointer. → T-80
|
||||
|
||||
25. **`lib/src/daemon/git_commands.dart:283`** — `_gitError` always
|
||||
reports `tool_error`. A `git push` rejection or merge conflict is
|
||||
user-actionable, not a tool failure; could map to
|
||||
`IpcExitCode.conflict` when stderr matches known patterns. → T-81
|
||||
|
||||
26. **`lib/src/ipc/server.dart:97`** — Dispatch error shows
|
||||
`dispatch failed: $e` (full exception toString). Trim and add
|
||||
the request `cmd` for log correlation. → T-80
|
||||
|
||||
27. **`lib/src/daemon/pane_commands.dart:136`** — `registry.write(id, bytes)`
|
||||
return value `n` is shown to caller, but if `n == -1` (write failed)
|
||||
we still respond `ok`. Distinguish. → T-81
|
||||
|
||||
28. **`lib/src/ipc/envelope.dart:88-94`** — `IpcResponse.fromJson`
|
||||
throws `TypeError` if `ok=false` but `error` is missing. No
|
||||
graceful degradation for a malformed peer response. → T-81
|
||||
|
||||
29. **`lib/src/pty/native_pty.dart:111-119`** — PATH resolution
|
||||
silently uses the first existing match without checking `X_OK`.
|
||||
A non-executable file shadows a valid binary further along PATH. → T-81
|
||||
@@ -1,4 +1,25 @@
|
||||
# clide · design handoff
|
||||
# clide · design handoff (superseded reference)
|
||||
|
||||
> **Status (2026-05-06):** Reference-only. The implementation has
|
||||
> moved past these mockups. The canonical wireframe set now lives at
|
||||
> [`docs/wireframes/`](../wireframes/), generated from the actual
|
||||
> implementation via the `frame0-wireframe` skill.
|
||||
>
|
||||
> Update wireframes there, not here.
|
||||
>
|
||||
> **Why kept:** the design tokens under `tokens/` and `themes/` still
|
||||
> feed the runtime themes (per [D-43](../../governance/decisions/architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
||||
> / [D-44](../../governance/decisions/architecture.md#d-44-four-bundled-themes-clide-midnight-paper-terminal)).
|
||||
> The HTMLs and PNGs are kept for historical context.
|
||||
>
|
||||
> **What changed since:** welcome screen has logo-with-wordmark and a
|
||||
> Tips card spanning both columns; status line with theme switcher
|
||||
> lives at the bottom right; Claude pane runs in fullscreen mode
|
||||
> (`CLAUDE_CODE_NO_FLICKER=1`) so the input box is pinned by Claude
|
||||
> Code itself; tmux uses an isolated `-L clide` socket with bundled
|
||||
> config; sidebar layout follows D-47's "Claude is home" model.
|
||||
|
||||
---
|
||||
|
||||
Bundle for importing into the clide repo and driving further work with Claude Code.
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Multitab pane — design
|
||||
|
||||
Ticket: T-83
|
||||
Drives: T-24 (secondary Claude pane UI wiring)
|
||||
Date: 2026-05-06
|
||||
|
||||
## Problem
|
||||
|
||||
Some panes need to host multiple, dynamically-spawned views of the
|
||||
same kind. The first concrete case is the Claude pane: per
|
||||
[D-41](../../governance/decisions/architecture.md#d-41-claude-panes-one-primary-per-repo-tmux-backed),
|
||||
each repo has exactly one **primary** Claude pane plus zero or more
|
||||
**secondary** panes spawned at runtime. The user needs a way to:
|
||||
|
||||
- See which Claude sessions are open
|
||||
- Switch between them
|
||||
- Spawn a new secondary
|
||||
- Close a secondary (primary has no close affordance)
|
||||
|
||||
The kernel's existing `TabContribution` system addresses a different
|
||||
need — it lets extensions statically declare which widget shows up in
|
||||
which **panel slot** (sidebar, workspace, context). It does not
|
||||
support dynamic tab instances *within* a single contribution.
|
||||
|
||||
This design fills that gap with a reusable widget, so future panes
|
||||
that need the same shape (potentially the editor — see D-48 — or
|
||||
diff/preview surfaces) can adopt it without reinventing tab strips.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing `TabContribution`. Slot-host tabs are static-by-design;
|
||||
this widget is for inside-a-tab dynamism.
|
||||
- Window-level tab management (browser-style "tear off into a window").
|
||||
- Editor multi-buffer tabs. [D-48](../../governance/decisions/architecture.md#d-48-chrome-budget-no-tabs-no-breadcrumbs-keyboard-first)
|
||||
rejected those; revisiting is a separate decision.
|
||||
|
||||
## API sketch
|
||||
|
||||
```dart
|
||||
class MultitabPane<T> extends StatefulWidget {
|
||||
const MultitabPane({
|
||||
required this.controller,
|
||||
required this.tabBuilder,
|
||||
required this.bodyBuilder,
|
||||
this.onCloseRequested,
|
||||
this.onAddRequested,
|
||||
this.allowReorder = true,
|
||||
});
|
||||
|
||||
final MultitabController<T> controller;
|
||||
final Widget Function(BuildContext, MultitabEntry<T>) tabBuilder;
|
||||
final Widget Function(BuildContext, MultitabEntry<T>) bodyBuilder;
|
||||
final void Function(MultitabEntry<T> entry)? onCloseRequested;
|
||||
final void Function()? onAddRequested;
|
||||
final bool allowReorder;
|
||||
}
|
||||
|
||||
class MultitabEntry<T> {
|
||||
final String id; // stable identity (e.g. "claude.primary")
|
||||
final String title; // display label
|
||||
final bool closeable; // primary tabs set this false
|
||||
final bool reorderable; // primary often pinned to position 0
|
||||
final T payload; // domain object the bodyBuilder renders
|
||||
}
|
||||
|
||||
class MultitabController<T> extends ChangeNotifier {
|
||||
List<MultitabEntry<T>> get entries;
|
||||
MultitabEntry<T>? get active;
|
||||
|
||||
void add(MultitabEntry<T> entry, {bool activate = true});
|
||||
void remove(String id);
|
||||
void activate(String id);
|
||||
void reorder(String id, int newIndex);
|
||||
}
|
||||
```
|
||||
|
||||
The widget is a thin shell:
|
||||
- Renders the tab strip via `ClideTabBar` (or a reorderable variant)
|
||||
- Calls `bodyBuilder(active)` for the visible content
|
||||
- Routes user gestures to controller methods or callbacks
|
||||
- Emits `onCloseRequested` / `onAddRequested` so the host decides
|
||||
the actual lifecycle (e.g. Claude pane spawns a new tmux session,
|
||||
doesn't just append a UI tab)
|
||||
|
||||
The host owns the controller and the payload type. The widget never
|
||||
touches PTY, IPC, or Claude session naming.
|
||||
|
||||
## Rendering
|
||||
|
||||
The tab strip lives at the top of the pane chrome. Layout:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ [primary] [secondary 1] [secondary 2] [+] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ active tab body │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Active tab: filled background, bright text
|
||||
- Inactive: muted background, muted text
|
||||
- Close glyph (×) appears on hover for `closeable` tabs
|
||||
- `+` button at the end if `onAddRequested` is set
|
||||
- Drag-to-reorder respects `reorderable`; non-reorderable tabs
|
||||
(primary) are pinned to position 0 and other tabs cannot be
|
||||
dropped before them
|
||||
|
||||
## Interaction
|
||||
|
||||
- **Click a tab** → activate
|
||||
- **Click ×** → call `onCloseRequested(entry)`; host decides whether
|
||||
to confirm, kill the underlying session, etc.
|
||||
- **Drag-and-drop** → call `controller.reorder(id, newIndex)` after
|
||||
the gesture completes; controller enforces pinned positions
|
||||
- **Click +** → call `onAddRequested()`; host creates the new entry
|
||||
and adds it via `controller.add(...)`
|
||||
- **Keyboard**: `⌘1`–`⌘9` jump to tab N; `⌘W` close active (skipped
|
||||
for non-closeable); `⌘⇧[` / `⌘⇧]` cycle prev/next
|
||||
|
||||
## Persistence
|
||||
|
||||
Out of scope for the widget. Hosts that want to persist tab order or
|
||||
which tabs were open across sessions read/write through their own
|
||||
settings layer and seed the controller on init.
|
||||
|
||||
## Claude pane integration (T-24)
|
||||
|
||||
```
|
||||
ClaudePane (host)
|
||||
└── MultitabPane<ClaudeSessionRef>(
|
||||
controller: claudeTabsController,
|
||||
tabBuilder: (ctx, e) => Text(e.title),
|
||||
bodyBuilder: (ctx, e) => ClaudePaneBody(session: e.payload),
|
||||
onAddRequested: () => kernel.claude.spawnSecondary(),
|
||||
onCloseRequested: (e) => kernel.claude.closeSecondary(e.payload),
|
||||
)
|
||||
```
|
||||
|
||||
`ClaudeSessionRef` carries the tmux session name + isPrimary. The
|
||||
controller is seeded with `[primary]` on boot; secondaries get
|
||||
appended as the user clicks `+`. Closing a secondary triggers
|
||||
`pane.close` IPC and removes the entry; closing the primary is not
|
||||
exposed (`closeable: false`).
|
||||
|
||||
## What ships in this ticket
|
||||
|
||||
T-83 delivers:
|
||||
1. `MultitabPane` widget + `MultitabController` + `MultitabEntry`
|
||||
under `lib/widgets/src/`
|
||||
2. Unit tests for controller invariants (pinned positions, active
|
||||
selection survives close, reorder bounds)
|
||||
3. Widget tests for the strip (selection, close hover, add button,
|
||||
drag-reorder)
|
||||
4. This design doc
|
||||
|
||||
T-24 picks up after and wires the Claude pane to it.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Q: Where does keyboard handling live?** Host or widget?
|
||||
Recommendation: widget owns `⌘W` / `⌘1`–`⌘9` / cycle; host wires
|
||||
them via the existing kernel commands surface. Avoids each host
|
||||
reinventing the same shortcuts.
|
||||
|
||||
**Nesting caveat:** the widget composes (a Claude tab can host
|
||||
its own `MultitabPane<EditorBuffer>` etc.). Shortcut handling
|
||||
must be scoped to the focus subtree, not registered globally —
|
||||
otherwise the outermost pane consumes `⌘W` even when the user
|
||||
is typing in a nested tab. Implementation: wrap shortcuts in a
|
||||
`Shortcuts` / `Actions` widget inside the pane's `Focus` scope
|
||||
so the innermost focused pane wins via Flutter's normal
|
||||
shortcut-resolution chain.
|
||||
|
||||
- **Q: Tab overflow** when many secondaries open? Recommendation:
|
||||
start with horizontal scroll; revisit if it becomes a problem.
|
||||
|
||||
- **Q: Tab-strip visual style** — match `ClideTabBar` exactly, or
|
||||
introduce a denser variant for inside-pane use? Recommendation:
|
||||
reuse `ClideTabBar` initially; spin off a `ClideTabBar.dense`
|
||||
variant only if visual hierarchy issues emerge.
|
||||
@@ -0,0 +1,166 @@
|
||||
title: MultitabPane — architecture {
|
||||
near: top-center
|
||||
shape: text
|
||||
style.font-size: 24
|
||||
style.bold: true
|
||||
}
|
||||
|
||||
direction: down
|
||||
|
||||
host: ClaudePane (host) {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#7c5cff"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
state: ChangeNotifier — owns lifecycle {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
widget: MultitabPane<T> (widget) {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
shell: builds tabstrip + body shell {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
controller: MultitabController<T> {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
api: |md
|
||||
add(entry)
|
||||
remove(id)
|
||||
activate(id)
|
||||
reorder(id, idx)
|
||||
| {
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
entries: List<MultitabEntry<T>> {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
|
||||
primary: primary {
|
||||
shape: rectangle
|
||||
style.fill: "#1a1f28"
|
||||
style.stroke: "#7c5cff"
|
||||
style.font-color: "#e8ecf2"
|
||||
closeable\: false: { shape: text; style.font-color: "#7a8294"; style.font-size: 10 }
|
||||
reorderable\: false: { shape: text; style.font-color: "#7a8294"; style.font-size: 10 }
|
||||
}
|
||||
sec1: secondary 1 {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
sec2: secondary 2 {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
ipc: kernel.claude / IPC {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#5a8c5a"
|
||||
style.font-color: "#e8ecf2"
|
||||
|
||||
spawn: spawnSecondary() {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
close: closeSecondary(ref) {
|
||||
shape: rectangle
|
||||
style.fill: "#0e1014"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
}
|
||||
|
||||
host -> controller: owns {
|
||||
style.stroke: "#7a8294"
|
||||
style.font-color: "#7a8294"
|
||||
}
|
||||
host -> widget: builds with {
|
||||
style.stroke: "#7a8294"
|
||||
style.font-color: "#7a8294"
|
||||
}
|
||||
controller -> entries: holds {
|
||||
style.stroke: "#7a8294"
|
||||
style.font-color: "#7a8294"
|
||||
}
|
||||
widget -> controller: subscribes (Listenable) {
|
||||
style.stroke: "#7c5cff"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
|
||||
widget -> host: onAddRequested() {
|
||||
style.stroke: "#d97757"
|
||||
style.font-color: "#d97757"
|
||||
}
|
||||
widget -> host: onCloseRequested(entry) {
|
||||
style.stroke: "#d97757"
|
||||
style.font-color: "#d97757"
|
||||
}
|
||||
|
||||
host -> ipc: routes user intent {
|
||||
style.stroke: "#5a8c5a"
|
||||
style.font-color: "#5a8c5a"
|
||||
}
|
||||
ipc.spawn -> entries.sec2: appends new entry {
|
||||
style.stroke: "#5a8c5a"
|
||||
style.font-color: "#5a8c5a"
|
||||
}
|
||||
ipc.close -> entries.sec1: removes entry {
|
||||
style.stroke: "#d97757"
|
||||
style.font-color: "#d97757"
|
||||
}
|
||||
|
||||
note: |md
|
||||
### Boundary
|
||||
|
||||
**Widget** is generic. It knows
|
||||
about `MultitabEntry<T>` and routes
|
||||
user gestures back to the host. It
|
||||
never touches PTY, IPC, or session
|
||||
naming.
|
||||
|
||||
**Host** owns the controller and
|
||||
decides what `add` / `close` mean
|
||||
in the domain — for Claude that's
|
||||
spawning/killing tmux sessions
|
||||
via IPC.
|
||||
|
||||
This boundary is what makes the
|
||||
widget reusable: any pane that
|
||||
needs N runtime instances can
|
||||
drop it in with their own host
|
||||
and payload type.
|
||||
| {
|
||||
shape: rectangle
|
||||
style.fill: "#13161c"
|
||||
style.stroke: "#262a32"
|
||||
style.font-color: "#a0a8b8"
|
||||
}
|
||||
|
After Width: | Height: | Size: 1009 KiB |
@@ -0,0 +1,53 @@
|
||||
# clide wireframes
|
||||
|
||||
Canonical layout reference, generated from the actual implementation
|
||||
via the `frame0-wireframe` skill. Each `.json` is the source of
|
||||
truth; the `.png` is rendered from it.
|
||||
|
||||
These supersede the hi-fi mockups under
|
||||
[`../claude-design/`](../claude-design/), which are kept for
|
||||
historical context and design tokens.
|
||||
|
||||
## Set
|
||||
|
||||
### Welcome
|
||||
- [`welcome/welcome-screen.json`](welcome/welcome-screen.json) /
|
||||
[.png](welcome/welcome-screen.png)
|
||||
— first-run landing: logo + wordmark, START / RECENT columns,
|
||||
Tips card, status line.
|
||||
|
||||
### Main view
|
||||
- [`main/main-view.json`](main/main-view.json) /
|
||||
[.png](main/main-view.png)
|
||||
— three-column default: tickets sidebar, Claude pane, empty
|
||||
context panel.
|
||||
- [`main/editor-above-claude.json`](main/editor-above-claude.json) /
|
||||
[.png](main/editor-above-claude.png)
|
||||
— D-49 editor mode: editor above Claude in the middle column,
|
||||
divider between, prompt Y stays fixed.
|
||||
- [`main/focus-mode.json`](main/focus-mode.json) /
|
||||
[.png](main/focus-mode.png)
|
||||
— D-52 focus mode: full-window Claude pane, sidebars hidden,
|
||||
Esc-to-exit hint in the title bar.
|
||||
- [`main/sidebar-collapsed.json`](main/sidebar-collapsed.json) /
|
||||
[.png](main/sidebar-collapsed.png)
|
||||
— D-51 12px spine: sidebar collapsed to a vertical strip with
|
||||
rotated label and activity badge.
|
||||
- [`main/ticket-detail.json`](main/ticket-detail.json) /
|
||||
[.png](main/ticket-detail.png)
|
||||
— context panel showing a selected ticket with metadata and
|
||||
description.
|
||||
|
||||
## Updating
|
||||
|
||||
1. Edit the `.json` (source of truth).
|
||||
2. Re-export with the `frame0-wireframe` skill:
|
||||
```
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-sync.py \
|
||||
export docs/wireframes/<dir>/<name>.json \
|
||||
docs/wireframes/<dir>/<name>.png
|
||||
```
|
||||
3. Commit both files.
|
||||
|
||||
Frame0 must be running locally for export. Don't pull from Frame0 —
|
||||
the JSON is authoritative.
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"name": "Claude Pane — multitab",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1100, "height": 720,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
|
||||
"pane-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1100, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"pane-title": {
|
||||
"type": "Text",
|
||||
"parent": "pane-header",
|
||||
"left": 56, "top": 50,
|
||||
"text": "claude — secondary 2",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"pane-subtitle": {
|
||||
"type": "Text",
|
||||
"parent": "pane-header",
|
||||
"left": 56, "top": 64,
|
||||
"text": "tmux · clide-claude-var-mnt-data-projects-clide-2",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
|
||||
"tabstrip": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 1100, "height": 32,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
|
||||
"tab-primary": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 56, "top": 80, "width": 132, "height": 28,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-primary-pin": {
|
||||
"type": "Text",
|
||||
"parent": "tab-primary",
|
||||
"left": 64, "top": 86,
|
||||
"text": "📌",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
"tab-primary-text": {
|
||||
"type": "Text",
|
||||
"parent": "tab-primary",
|
||||
"left": 84, "top": 86,
|
||||
"text": "primary",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"tab-sec-1": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 192, "top": 80, "width": 132, "height": 28,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-sec-1-text": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-1",
|
||||
"left": 204, "top": 86,
|
||||
"text": "secondary 1",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tab-sec-1-close": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-1",
|
||||
"left": 304, "top": 86,
|
||||
"text": "×",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"tab-sec-2": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 328, "top": 80, "width": 132, "height": 28,
|
||||
"fillColor": "#1a1f28",
|
||||
"strokeColor": "#7c5cff",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-sec-2-text": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-2",
|
||||
"left": 340, "top": 86,
|
||||
"text": "secondary 2",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tab-sec-2-close": {
|
||||
"type": "Text",
|
||||
"parent": "tab-sec-2",
|
||||
"left": 440, "top": 86,
|
||||
"text": "×",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"tab-add": {
|
||||
"type": "Rectangle",
|
||||
"parent": "tabstrip",
|
||||
"left": 464, "top": 80, "width": 28, "height": 28,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"tab-add-glyph": {
|
||||
"type": "Text",
|
||||
"parent": "tab-add",
|
||||
"left": 474, "top": 86,
|
||||
"text": "+",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"active-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 108, "width": 1100, "height": 2,
|
||||
"fillColor": "#7c5cff",
|
||||
"strokeColor": "#7c5cff"
|
||||
},
|
||||
|
||||
"body": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 110, "width": 1100, "height": 650,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"body-banner-name": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 132,
|
||||
"text": "Claude Code v2.1.128",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"body-banner-meta": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 148,
|
||||
"text": "Opus 4.7 (1M context) · fresh secondary session",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"body-msg-prompt": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 200,
|
||||
"text": "› dig into the failing test in test/pty/session_test.dart",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"body-msg-resp": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 226,
|
||||
"text": "● Looking at the write-keystrokes test. The shell process\n starts but the echo doesn't appear in the output stream.\n Let me trace the write path…",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "body",
|
||||
"left": 40, "top": 700, "width": 1100, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "Text",
|
||||
"parent": "body",
|
||||
"left": 56, "top": 712,
|
||||
"text": "› Try \"run the test in this pane\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"anno": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 720, "top": 80,
|
||||
"text": "active tab gets accent border + bottom rule",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"name": "Main View — editor above Claude",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 860,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 36,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar-text": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 720, "top": 50,
|
||||
"text": "clide › clide ⌄ · src/welcome/welcome_view.dart ●",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 280, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"sidebar-section": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 96,
|
||||
"text": "▾ FILES",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"files-tree": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 124,
|
||||
"text": "▸ assets\n▸ bin\n▾ lib\n ▾ builtin\n ▾ welcome\n ▾ src\n welcome_view.dart\n extension.dart\n ▸ kernel\n ▸ widgets",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"sidebar-rail": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 822, "width": 280, "height": 40,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
|
||||
"editor-pane": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 320, "top": 76, "width": 740, "height": 320,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"editor-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "editor-pane",
|
||||
"left": 320, "top": 76, "width": 740, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"editor-title": {
|
||||
"type": "Text",
|
||||
"parent": "editor-header",
|
||||
"left": 336, "top": 86,
|
||||
"text": "✎ welcome_view.dart ●",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"editor-demote": {
|
||||
"type": "Text",
|
||||
"parent": "editor-header",
|
||||
"left": 1024, "top": 86,
|
||||
"text": "👁 ⌘W",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"editor-line-1": {
|
||||
"type": "Text",
|
||||
"parent": "editor-pane",
|
||||
"left": 336, "top": 130,
|
||||
"text": " 1 class _TipsCard extends StatelessWidget {",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
"editor-line-2": {
|
||||
"type": "Text",
|
||||
"parent": "editor-pane",
|
||||
"left": 336, "top": 148,
|
||||
"text": " 2 const _TipsCard({required this.tokens});",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
"editor-line-3": {
|
||||
"type": "Text",
|
||||
"parent": "editor-pane",
|
||||
"left": 336, "top": 166,
|
||||
"text": " 3 final SurfaceTokens tokens;",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
"editor-line-4": {
|
||||
"type": "Text",
|
||||
"parent": "editor-pane",
|
||||
"left": 336, "top": 184,
|
||||
"text": " 4",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 11
|
||||
},
|
||||
"editor-line-5": {
|
||||
"type": "Text",
|
||||
"parent": "editor-pane",
|
||||
"left": 336, "top": 202,
|
||||
"text": " 5 static const _tips = <(String, String)>[",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
"editor-line-6": {
|
||||
"type": "Text",
|
||||
"parent": "editor-pane",
|
||||
"left": 336, "top": 220,
|
||||
"text": " 6 ('Quick open', '⌘P'),",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 320, "top": 396, "width": 740, "height": 4,
|
||||
"fillColor": "#262a32",
|
||||
"strokeColor": "#262a32"
|
||||
},
|
||||
|
||||
"claude-pane": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 320, "top": 400, "width": 740, "height": 462,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 320, "top": 400, "width": 740, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-title": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 336, "top": 410,
|
||||
"text": "claude — primary",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-conv": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 336, "top": 460,
|
||||
"text": "› refactor _TipsCard into its own file\n\n● Moved _TipsCard to lib/builtin/welcome/src/tips_card.dart.\n Re-exported from welcome_view.dart for backwards compat.\n Updated test/builtin/welcome/widget_test.dart to import\n the new path.",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 320, "top": 800, "width": 740, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-prompt": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 336, "top": 808,
|
||||
"text": "› Try \"run the tests\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"context-panel": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 1060, "top": 76, "width": 380, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"context-section": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 96,
|
||||
"text": "▾ PREVIEW",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"context-doc": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 134,
|
||||
"text": "Welcome screen\n\nThe first surface a user sees when no project is\nopen, or after closing one.\n\nLayout: centered max-850px column with logo +\nwordmark, START / RECENT row, and (when the\nviewport is tall enough) a TIPS card spanning\nthe full width.",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"status-branch": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 56, "top": 880,
|
||||
"text": "⑂ main ↑5 · 3 modified",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"status-app": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 700, "top": 880,
|
||||
"text": "● application ok",
|
||||
"fontColor": "#5a8c5a",
|
||||
"fontSize": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"name": "Main View — focus mode",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 860,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 36,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar-text": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 720, "top": 50,
|
||||
"text": "clide › clide ⌄ — focus mode",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"esc-hint": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 1340, "top": 50,
|
||||
"text": "Esc to exit",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"claude-pane": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 1400, "height": 786,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 40, "top": 76, "width": 1400, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-title": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 56, "top": 86,
|
||||
"text": "claude — primary",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-subtitle": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 56, "top": 100,
|
||||
"text": "tmux · clide-claude-var-mnt-data-projects-clide · focus",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
|
||||
"claude-banner-icon": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 56, "top": 142,
|
||||
"text": "▰▰",
|
||||
"fontColor": "#d97757",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-banner-name": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 82, "top": 142,
|
||||
"text": "Claude Code v2.1.128",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-banner-meta": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 82, "top": 158,
|
||||
"text": "Opus 4.7 (1M context) with high effort · Claude Max",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"claude-msg-1-prompt": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 56, "top": 230,
|
||||
"text": "› switch to focus mode for the next push",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-msg-1-resp": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 56, "top": 258,
|
||||
"text": "● Focus mode entered. Esc to exit. Sidebar and context\n panel are hidden — Claude pane fills the workspace.",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"claude-prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 40, "top": 800, "width": 1400, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-prompt-caret": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 56, "top": 808,
|
||||
"text": "›",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"claude-prompt-placeholder": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 80, "top": 810,
|
||||
"text": "Try \"go full screen\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,447 @@
|
||||
{
|
||||
"name": "Main View — project loaded",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 860,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
|
||||
"title-bar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 36,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"title-bar-text": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 720, "top": 50,
|
||||
"text": "clide › clide ⌄",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 360, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"sidebar-search": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 92, "width": 280, "height": 32,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#262a32",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"sidebar-search-icon": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-search",
|
||||
"left": 68, "top": 100,
|
||||
"text": "⌕",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
"section-in-progress": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 142,
|
||||
"text": "▾ IN PROGRESS · 1",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-1": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 166, "width": 328, "height": 64,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#262a32",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"ticket-1-id": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-1",
|
||||
"left": 70, "top": 178,
|
||||
"text": "● T-21 ← T-4",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-1-title": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-1",
|
||||
"left": 70, "top": 200,
|
||||
"text": "implement welcome screen per hi-fi design",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 13
|
||||
},
|
||||
|
||||
"section-ready": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 250,
|
||||
"text": "▾ READY · 3",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-2": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 274, "width": 328, "height": 56,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"ticket-2-id": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-2",
|
||||
"left": 70, "top": 282,
|
||||
"text": "● T-17 ← T-8",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-2-title": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-2",
|
||||
"left": 70, "top": 302,
|
||||
"text": "add dart doc generation to CI",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"ticket-3": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 332, "width": 328, "height": 56,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"ticket-3-id": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-3",
|
||||
"left": 70, "top": 340,
|
||||
"text": "● T-24 ← T-3",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-3-title": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-3",
|
||||
"left": 70, "top": 360,
|
||||
"text": "secondary Claude pane UI wiring",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"section-backlog": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 408,
|
||||
"text": "▾ BACKLOG · 33",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-4": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 432, "width": 328, "height": 50,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"ticket-4-id": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-4",
|
||||
"left": 70, "top": 440,
|
||||
"text": "● T-7 ← T-7",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-4-title": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-4",
|
||||
"left": 70, "top": 458,
|
||||
"text": "Tier 5 — canvas and graph view",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"ticket-5": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 484, "width": 328, "height": 50,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"ticket-5-id": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-5",
|
||||
"left": 70, "top": 492,
|
||||
"text": "● T-23 ← T-4",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-5-title": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-5",
|
||||
"left": 70, "top": 510,
|
||||
"text": "wire command palette keybinding",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"sidebar-rail": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 822, "width": 360, "height": 40,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"rail-icon-1": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-rail",
|
||||
"left": 68, "top": 832,
|
||||
"text": "▤",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 16
|
||||
},
|
||||
"rail-icon-2": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-rail",
|
||||
"left": 110, "top": 832,
|
||||
"text": "◇",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"rail-icon-3": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-rail",
|
||||
"left": 152, "top": 832,
|
||||
"text": "▢",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"rail-icon-4": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-rail",
|
||||
"left": 194, "top": 832,
|
||||
"text": "⑂",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"rail-icon-5": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-rail",
|
||||
"left": 236, "top": 832,
|
||||
"text": "⌕",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"rail-icon-6": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar-rail",
|
||||
"left": 278, "top": 832,
|
||||
"text": "!",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
|
||||
"claude-pane": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 400, "top": 76, "width": 660, "height": 786,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 400, "top": 76, "width": 660, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-title": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 416, "top": 86,
|
||||
"text": "claude — primary",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-subtitle": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 416, "top": 100,
|
||||
"text": "tmux · clide-claude-var-mnt-data-projects-clide",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
|
||||
"claude-logo-icon": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 142,
|
||||
"text": "▰▰",
|
||||
"fontColor": "#d97757",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-banner-name": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 442, "top": 142,
|
||||
"text": "Claude Code v2.1.128",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-banner-model": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 442, "top": 158,
|
||||
"text": "Opus 4.7 (1M context) with high effort · Claude Max",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"claude-banner-cwd": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 442, "top": 174,
|
||||
"text": "/var/mnt/data/projects/clide",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"claude-message-1": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 230,
|
||||
"text": "› wireframe the main view",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-response-1": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 258,
|
||||
"text": "● Building wireframe from current implementation. Three\n columns: sidebar (tickets shown), Claude pane, context\n panel. Let me check the tab contributions first.",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"claude-mcp-warn": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 720,
|
||||
"text": "1 MCP server failed · /mcp",
|
||||
"fontColor": "#d97757",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"claude-prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 400, "top": 752, "width": 660, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-prompt-caret": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 760,
|
||||
"text": "›",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"claude-prompt-placeholder": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 440, "top": 762,
|
||||
"text": "Try \"export the wireframes\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-prompt-meta": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 794,
|
||||
"text": "[jeroenschweitzer@danoontje clide] | Opus 4.7 (1M context)",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
|
||||
"context-panel": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 1060, "top": 76, "width": 380, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"context-empty": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 96,
|
||||
"text": "Select a ticket to view details.",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"context-rail": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 1060, "top": 822, "width": 380, "height": 40,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"context-rail-icon-1": {
|
||||
"type": "Text",
|
||||
"parent": "context-rail",
|
||||
"left": 1300, "top": 832,
|
||||
"text": "▤",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 16
|
||||
},
|
||||
"context-rail-icon-2": {
|
||||
"type": "Text",
|
||||
"parent": "context-rail",
|
||||
"left": 1340, "top": 832,
|
||||
"text": "◇",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"context-rail-icon-3": {
|
||||
"type": "Text",
|
||||
"parent": "context-rail",
|
||||
"left": 1380, "top": 832,
|
||||
"text": "⌕",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
"context-rail-icon-4": {
|
||||
"type": "Text",
|
||||
"parent": "context-rail",
|
||||
"left": 1416, "top": 832,
|
||||
"text": "▢",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
|
||||
"status-branch": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 56, "top": 880,
|
||||
"text": "⑂ main ↑5",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"status-app": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 700, "top": 880,
|
||||
"text": "● application ok",
|
||||
"fontColor": "#5a8c5a",
|
||||
"fontSize": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"name": "Main View — sidebar collapsed",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 860,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 36,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar-text": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 720, "top": 50,
|
||||
"text": "clide › clide ⌄",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"spine-left": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 12, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"spine-left-label": {
|
||||
"type": "Text",
|
||||
"parent": "spine-left",
|
||||
"left": 44, "top": 220,
|
||||
"text": "TICKETS",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 9
|
||||
},
|
||||
"spine-left-badge": {
|
||||
"type": "Ellipse",
|
||||
"parent": "spine-left",
|
||||
"left": 43, "top": 96, "width": 6, "height": 6,
|
||||
"fillColor": "#d97757",
|
||||
"strokeColor": "#d97757"
|
||||
},
|
||||
|
||||
"claude-pane": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 52, "top": 76, "width": 1008, "height": 786,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 52, "top": 76, "width": 1008, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-title": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 68, "top": 86,
|
||||
"text": "claude — primary",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-subtitle": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 68, "top": 100,
|
||||
"text": "tmux · sidebar collapsed (⌘⇧1) — Tickets has activity",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 10
|
||||
},
|
||||
|
||||
"claude-banner": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 68, "top": 142,
|
||||
"text": "Claude Code v2.1.128 · Opus 4.7 (1M context)",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"claude-msg": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 68, "top": 200,
|
||||
"text": "› hide the sidebar, I want more room\n\n● Sidebar collapsed to a 12px spine. Activity badge\n on the spine indicates new ticket changes — click\n the spine or press ⌘⇧1 to expand.",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"claude-prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 52, "top": 800, "width": 1008, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-prompt": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 68, "top": 808,
|
||||
"text": "› Try \"show the sidebar again\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"context-panel": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 1060, "top": 76, "width": 380, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"context-section": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 96,
|
||||
"text": "▾ VIEWER",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"context-empty": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 134,
|
||||
"text": "Open a file to preview.",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"status-branch": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 56, "top": 880,
|
||||
"text": "⑂ main ↑5",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"status-app": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 700, "top": 880,
|
||||
"text": "● application ok",
|
||||
"fontColor": "#5a8c5a",
|
||||
"fontSize": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"name": "Main View — ticket detail in context",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 860,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1400, "height": 36,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"title-bar-text": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 720, "top": 50,
|
||||
"text": "clide › clide ⌄",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"sidebar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 360, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"section": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 96,
|
||||
"text": "▾ IN PROGRESS · 1",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-selected": {
|
||||
"type": "Rectangle",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 122, "width": 328, "height": 64,
|
||||
"fillColor": "#1a1f28",
|
||||
"strokeColor": "#7c5cff",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"ticket-selected-id": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-selected",
|
||||
"left": 70, "top": 134,
|
||||
"text": "● T-24 ← T-3",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-selected-title": {
|
||||
"type": "Text",
|
||||
"parent": "ticket-selected",
|
||||
"left": 70, "top": 156,
|
||||
"text": "secondary Claude pane UI wiring",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 13
|
||||
},
|
||||
|
||||
"section-2": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 56, "top": 210,
|
||||
"text": "▾ READY · 3",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-other-1": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 70, "top": 240,
|
||||
"text": "● T-17 add dart doc generation to CI",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"ticket-other-2": {
|
||||
"type": "Text",
|
||||
"parent": "sidebar",
|
||||
"left": 70, "top": 264,
|
||||
"text": "● T-21 implement welcome screen…",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"claude-pane": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 400, "top": 76, "width": 540, "height": 786,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-header": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 400, "top": 76, "width": 540, "height": 36,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-title": {
|
||||
"type": "Text",
|
||||
"parent": "claude-header",
|
||||
"left": 416, "top": 86,
|
||||
"text": "claude — primary",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-msg": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 142,
|
||||
"text": "› let's pick T-24 next\n\n● T-24 selected. Detail loaded in the right panel.\n This ticket wires up the secondary Claude pane UI —\n see D-41 for the spawn/close semantics.",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
"claude-prompt-divider": {
|
||||
"type": "Rectangle",
|
||||
"parent": "claude-pane",
|
||||
"left": 400, "top": 800, "width": 540, "height": 1,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"claude-prompt": {
|
||||
"type": "Text",
|
||||
"parent": "claude-pane",
|
||||
"left": 416, "top": 808,
|
||||
"text": "› Try \"start it\"",
|
||||
"fontColor": "#5a6478",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"context-panel": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 940, "top": 76, "width": 500, "height": 786,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"ticket-id": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 96,
|
||||
"text": "T-24 · task · ready",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"ticket-title": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 118,
|
||||
"text": "secondary Claude pane UI wiring",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 18
|
||||
},
|
||||
|
||||
"field-parent": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 162,
|
||||
"text": "Parent",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"field-parent-val": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 162,
|
||||
"text": "T-3 — Tier 1 — Claude in xterm pane, PTY, session",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"field-decision": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 188,
|
||||
"text": "Decision",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"field-decision-val": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 188,
|
||||
"text": "D-41 — Claude panes — one primary per repo, tmux-backed",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"field-priority": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 214,
|
||||
"text": "Priority",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"field-priority-val": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 214,
|
||||
"text": "medium",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"field-created": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 240,
|
||||
"text": "Created",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"field-created-val": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 1080, "top": 240,
|
||||
"text": "2026-04-22",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"desc-label": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 296,
|
||||
"text": "DESCRIPTION",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"desc-body": {
|
||||
"type": "Text",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 324,
|
||||
"text": "Wire the secondary Claude pane spawn flow into the\nUI: a “new Claude session” affordance in the pane\nchrome, secondary numbering (-1, -2 …), close-to-\nprimary focus collapse, and the visual distinction\nbetween primary and secondary in the pane header.",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"actions": {
|
||||
"type": "Rectangle",
|
||||
"parent": "context-panel",
|
||||
"left": 956, "top": 760, "width": 468, "height": 36,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#262a32",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"action-start": {
|
||||
"type": "Text",
|
||||
"parent": "actions",
|
||||
"left": 974, "top": 770,
|
||||
"text": "▶ Start (status → in_progress)",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"status-branch": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 56, "top": 880,
|
||||
"text": "⑂ main ↑5",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"name": "Welcome Screen",
|
||||
"shapes": {
|
||||
"canvas": {
|
||||
"type": "Rectangle",
|
||||
"left": 40, "top": 40, "width": 1280, "height": 800,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
|
||||
"title-bar": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 40, "width": 1280, "height": 36,
|
||||
"fillColor": "#1c2028",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 0, 0]
|
||||
},
|
||||
"title-bar-text": {
|
||||
"type": "Text",
|
||||
"parent": "title-bar",
|
||||
"left": 660, "top": 50,
|
||||
"text": "clide ⌄",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"spine-left": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 40, "top": 76, "width": 12, "height": 700,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
"spine-right": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 1308, "top": 76, "width": 12, "height": 700,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028"
|
||||
},
|
||||
|
||||
"logo": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 250, "top": 240,
|
||||
"text": "<>",
|
||||
"fontColor": "#7c8896",
|
||||
"fontSize": 96
|
||||
},
|
||||
"logo-accent": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 254, "top": 332, "width": 60, "height": 4,
|
||||
"fillColor": "#d97757",
|
||||
"strokeColor": "#d97757"
|
||||
},
|
||||
|
||||
"wordmark": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 396, "top": 248,
|
||||
"text": "clide",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 64
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 396, "top": 322,
|
||||
"text": "IDE for Claude Code CLI",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 16
|
||||
},
|
||||
|
||||
"start-label": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 250, "top": 426,
|
||||
"text": "START",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"row-open": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 250, "top": 458, "width": 380, "height": 36,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#0e1014"
|
||||
},
|
||||
"row-open-icon": {
|
||||
"type": "Text",
|
||||
"parent": "row-open",
|
||||
"left": 262, "top": 466,
|
||||
"text": "▢",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 14
|
||||
},
|
||||
"row-open-label": {
|
||||
"type": "Text",
|
||||
"parent": "row-open",
|
||||
"left": 296, "top": 468,
|
||||
"text": "Open folder…",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 14
|
||||
},
|
||||
"row-open-key": {
|
||||
"type": "Text",
|
||||
"parent": "row-open",
|
||||
"left": 590, "top": 470,
|
||||
"text": "⌘O",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"row-clone": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 250, "top": 498, "width": 380, "height": 36,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#0e1014"
|
||||
},
|
||||
"row-clone-icon": {
|
||||
"type": "Text",
|
||||
"parent": "row-clone",
|
||||
"left": 262, "top": 506,
|
||||
"text": "⑂",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 14
|
||||
},
|
||||
"row-clone-label": {
|
||||
"type": "Text",
|
||||
"parent": "row-clone",
|
||||
"left": 296, "top": 508,
|
||||
"text": "Clone from git…",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 14
|
||||
},
|
||||
"row-clone-key": {
|
||||
"type": "Text",
|
||||
"parent": "row-clone",
|
||||
"left": 590, "top": 510,
|
||||
"text": "⌘G",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"row-claude": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 250, "top": 538, "width": 380, "height": 36,
|
||||
"fillColor": "#0e1014",
|
||||
"strokeColor": "#0e1014"
|
||||
},
|
||||
"row-claude-icon": {
|
||||
"type": "Text",
|
||||
"parent": "row-claude",
|
||||
"left": 262, "top": 546,
|
||||
"text": "◯",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 14
|
||||
},
|
||||
"row-claude-label": {
|
||||
"type": "Text",
|
||||
"parent": "row-claude",
|
||||
"left": 296, "top": 548,
|
||||
"text": "Start a Claude session",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 14
|
||||
},
|
||||
"row-claude-key": {
|
||||
"type": "Text",
|
||||
"parent": "row-claude",
|
||||
"left": 590, "top": 550,
|
||||
"text": "⌘C",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"recent-label": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 700, "top": 426,
|
||||
"text": "RECENT",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"recent-row": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 700, "top": 458, "width": 410, "height": 56,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"recent-name": {
|
||||
"type": "Text",
|
||||
"parent": "recent-row",
|
||||
"left": 716, "top": 468,
|
||||
"text": "clide",
|
||||
"fontColor": "#e8ecf2",
|
||||
"fontSize": 14
|
||||
},
|
||||
"recent-meta": {
|
||||
"type": "Text",
|
||||
"parent": "recent-row",
|
||||
"left": 716, "top": 490,
|
||||
"text": "/var/mnt/data/projects/clide · ⑂ main",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
"recent-time": {
|
||||
"type": "Text",
|
||||
"parent": "recent-row",
|
||||
"left": 1056, "top": 480,
|
||||
"text": "just now",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"tips-card": {
|
||||
"type": "Rectangle",
|
||||
"parent": "canvas",
|
||||
"left": 250, "top": 612, "width": 860, "height": 110,
|
||||
"fillColor": "#13161c",
|
||||
"strokeColor": "#1c2028",
|
||||
"corners": [6, 6, 6, 6]
|
||||
},
|
||||
"tips-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 274, "top": 628,
|
||||
"text": "TIPS",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
|
||||
"tip-1-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 274, "top": 660,
|
||||
"text": "Quick open",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 13
|
||||
},
|
||||
"tip-1-key": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 488, "top": 662,
|
||||
"text": "⌘P",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tip-2-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 558, "top": 660,
|
||||
"text": "Command palette",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 13
|
||||
},
|
||||
"tip-2-key": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 766, "top": 662,
|
||||
"text": "⌘⇧P",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tip-3-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 836, "top": 660,
|
||||
"text": "Toggle sidebar",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 13
|
||||
},
|
||||
"tip-3-key": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 1056, "top": 662,
|
||||
"text": "⌘B",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"tip-4-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 274, "top": 690,
|
||||
"text": "Toggle context",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 13
|
||||
},
|
||||
"tip-4-key": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 488, "top": 692,
|
||||
"text": "⌘J",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tip-5-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 558, "top": 690,
|
||||
"text": "Switch theme",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 13
|
||||
},
|
||||
"tip-5-key": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 754, "top": 692,
|
||||
"text": "⌘K ⌘T",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
"tip-6-label": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 836, "top": 690,
|
||||
"text": "New Claude session",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 13
|
||||
},
|
||||
"tip-6-key": {
|
||||
"type": "Text",
|
||||
"parent": "tips-card",
|
||||
"left": 1054, "top": 692,
|
||||
"text": "⌘⇧C",
|
||||
"fontColor": "#a0a8b8",
|
||||
"fontSize": 12
|
||||
},
|
||||
|
||||
"status-version": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 940, "top": 798,
|
||||
"text": "clide 2.0.0-dev",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
},
|
||||
"status-app": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 1058, "top": 798,
|
||||
"text": "· application ok",
|
||||
"fontColor": "#5a8c5a",
|
||||
"fontSize": 11
|
||||
},
|
||||
"status-theme": {
|
||||
"type": "Text",
|
||||
"parent": "canvas",
|
||||
"left": 1180, "top": 798,
|
||||
"text": "· theme: clide",
|
||||
"fontColor": "#7a8294",
|
||||
"fontSize": 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 80 KiB |
@@ -0,0 +1,158 @@
|
||||
# Decisions, Questions, Rejected
|
||||
|
||||
This directory holds structured planning records that pql parses
|
||||
into pql.db. Each record is a `### [DQR]-N: Title` heading inside
|
||||
a markdown file. Files live in three per-type subdirectories:
|
||||
|
||||
- `decisions/<domain>.md` — confirmed design decisions
|
||||
- `questions/<domain>.md` — open questions that may resolve into
|
||||
decisions or rejected proposals
|
||||
- `rejected/<domain>.md` — rejected proposals (kept for the audit
|
||||
trail)
|
||||
|
||||
The parser infers domain from the filename stem and record type
|
||||
from the parent subdirectory.
|
||||
|
||||
D-records that propose implementation work link to `initiative`-type
|
||||
tickets via `decision_ref`. Run `pql decisions show <id>
|
||||
--with-tickets` to inspect implementation status.
|
||||
|
||||
## Recommended domains
|
||||
|
||||
Start with this canonical set; create files as records land in
|
||||
each domain:
|
||||
|
||||
- **architecture** — structural commitments (storage, layering,
|
||||
languages, libraries)
|
||||
- **process** — team workflow (commits, branches, releases, reviews)
|
||||
- **design** — user-facing surface (UX, UI, public APIs)
|
||||
- **coding-conventions** — team-internal code shape (style, lint,
|
||||
file layout)
|
||||
- **testing** — quality strategy (coverage, layers, gates)
|
||||
|
||||
You might also want, project-permitting:
|
||||
|
||||
- `accessibility` — if you ship user-facing software
|
||||
- `security` — if you handle user data or network surfaces
|
||||
- `licensing` — if you release open-source or commercial
|
||||
- `documentation` — if user-docs are non-trivial
|
||||
- `deployment` — if shipping is non-trivial
|
||||
- `performance` — if you have perf budgets / SLOs
|
||||
|
||||
<!-- pql:records (auto-generated; do not edit manually) -->
|
||||
|
||||
## Decisions
|
||||
|
||||
- [D-1: CLI-first, not MCP](decisions/architecture.md#d-1-cli-first-not-mcp) — _architecture_
|
||||
- [D-3: pql as supporter tool; clide wraps, never duplicates](decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates) — _architecture_
|
||||
- [D-4: Ignore file strategy](decisions/architecture.md#d-4-ignore-file-strategy) — _architecture_
|
||||
- [D-5: Dart core; sidecar dissolved; `ptyc` as pql-peer](decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) — _architecture_
|
||||
- [D-6: CLI and event surface contract](decisions/architecture.md#d-6-cli-and-event-surface-contract) — _architecture_
|
||||
- [D-7: App root is bare `WidgetsApp`](decisions/architecture.md#d-7-app-root-is-bare-widgetsapp) — _architecture_
|
||||
- [D-8: Feature-first folder layout](decisions/architecture.md#d-8-feature-first-folder-layout) — _architecture_
|
||||
- [D-9: Three-tier theme pipeline](decisions/architecture.md#d-9-three-tier-theme-pipeline) — _architecture_
|
||||
- [D-10: State management — `ChangeNotifier` + `ListenableBuilder`](decisions/architecture.md#d-10-state-management--changenotifier--listenablebuilder) — _architecture_
|
||||
- [D-11: Panel manager is kernel; layout is data; three-column is a preset](decisions/architecture.md#d-11-panel-manager-is-kernel-layout-is-data-three-column-is-a-preset) — _architecture_
|
||||
- [D-12: Kernel admission rule — mandatory shared singletons only](decisions/architecture.md#d-12-kernel-admission-rule--mandatory-shared-singletons-only) — _architecture_
|
||||
- [D-13: Git hardcoded in kernel project-loader](decisions/architecture.md#d-13-git-hardcoded-in-kernel-project-loader) — _architecture_
|
||||
- [D-14: Two-tier disable — kernel locked, everything else extension-shaped](decisions/architecture.md#d-14-two-tier-disable--kernel-locked-everything-else-extension-shaped) — _architecture_
|
||||
- [D-15: Extension grain — container-level, multi-contribution](decisions/extensions.md#d-15-extension-grain--container-level-multi-contribution) — _extensions_
|
||||
- [D-16: Built-ins in Dart, third-party in sandboxed Lua](decisions/extensions.md#d-16-built-ins-in-dart-third-party-in-sandboxed-lua) — _extensions_
|
||||
- [D-17: Panels are extension-shaped from day one](decisions/extensions.md#d-17-panels-are-extension-shaped-from-day-one) — _extensions_
|
||||
- [D-18: YAML for themes + manifests; JSON for i18n catalogs](decisions/extensions.md#d-18-yaml-for-themes--manifests-json-for-i18n-catalogs) — _extensions_
|
||||
- [D-19: Lua runtime as `ptyc`-peer supporter tool](decisions/extensions.md#d-19-lua-runtime-as-ptyc-peer-supporter-tool) — _extensions_
|
||||
- [D-20: A11y is a Tier-0 contract](decisions/accessibility.md#d-20-a11y-is-a-tier-0-contract) — _accessibility_
|
||||
- [D-21: i18n is a Tier-0 contract (fframe pattern + locale-fallback chain)](decisions/accessibility.md#d-21-i18n-is-a-tier-0-contract-fframe-pattern--locale-fallback-chain) — _accessibility_
|
||||
- [D-22: WCAG-AA contrast gate on bundled themes](decisions/accessibility.md#d-22-wcag-aa-contrast-gate-on-bundled-themes) — _accessibility_
|
||||
- [D-23: Test pyramid — seven layers](decisions/testing.md#d-23-test-pyramid--seven-layers) — _testing_
|
||||
- [D-24: Golden tests — primitives only, Alchemist + Ahem](decisions/testing.md#d-24-golden-tests--primitives-only-alchemist--ahem) — _testing_
|
||||
- [D-25: Mocks — mocktail at IO, hand-rolled fakes for ChangeNotifiers](decisions/testing.md#d-25-mocks--mocktail-at-io-hand-rolled-fakes-for-changenotifiers) — _testing_
|
||||
- [D-26: Web driver — raw Playwright + Flutter semantics](decisions/testing.md#d-26-web-driver--raw-playwright--flutter-semantics) — _testing_
|
||||
- [D-27: Startup regression gate](decisions/testing.md#d-27-startup-regression-gate) — _testing_
|
||||
- [D-28: Test organisation — mirror `lib/` in `test/`](decisions/testing.md#d-28-test-organisation--mirror-lib-in-test) — _testing_
|
||||
- [D-29: Pre-push gate — fast layer only](decisions/testing.md#d-29-pre-push-gate--fast-layer-only) — _testing_
|
||||
- [D-30: Tests are client-side only](decisions/testing.md#d-30-tests-are-client-side-only) — _testing_
|
||||
- [D-31: Prefer-zero-deps, exact-pin](decisions/tooling.md#d-31-prefer-zero-deps-exact-pin) — _tooling_
|
||||
- [D-32: CI — Gitea primary, Linux-only runners, not yet activated](decisions/tooling.md#d-32-ci--gitea-primary-linux-only-runners-not-yet-activated) — _tooling_
|
||||
- [D-33: Golden-output ignore pattern — `coverage.*` excludes output, not scripts](decisions/tooling.md#d-33-golden-output-ignore-pattern--coverage-excludes-output-not-scripts) — _tooling_
|
||||
- [D-34: Q&D record system](decisions/process.md#d-34-qd-record-system) — _process_
|
||||
- [D-35: Kanban / waterfall, not Scrum](decisions/process.md#d-35-kanban--waterfall-not-scrum) — _process_
|
||||
- [D-36: `.claude/` is committed project surface, managed through the IDE](decisions/process.md#d-36-claude-is-committed-project-surface-managed-through-the-ide) — _process_
|
||||
- [D-37: Commit conventions per git-commit skill](decisions/process.md#d-37-commit-conventions-per-git-commit-skill) — _process_
|
||||
- [D-38: Changelog discipline — Keep a Changelog 1.1.0](decisions/process.md#d-38-changelog-discipline--keep-a-changelog-110) — _process_
|
||||
- [D-39: Planning tooling lives in pql, not clide](decisions/process.md#d-39-planning-tooling-lives-in-pql-not-clide) — _process_
|
||||
- [D-40: [SUPERSEDED] Python stopgap under `tools/scripts/plan`](decisions/process.md#d-40-superseded-python-stopgap-under-toolsscriptsplan) — _process_
|
||||
- [D-41: Claude panes — one primary per repo, tmux-backed](decisions/architecture.md#d-41-claude-panes--one-primary-per-repo-tmux-backed) — _architecture_
|
||||
- [D-42: Dependencies documented in `licenses.yaml`](decisions/tooling.md#d-42-dependencies-documented-in-licensesyaml) — _tooling_
|
||||
- [D-43: Design handoff — adopt token palettes, reject Material wrapper](decisions/architecture.md#d-43-design-handoff--adopt-token-palettes-reject-material-wrapper) — _architecture_
|
||||
- [D-44: Four bundled themes — clide, midnight, paper, terminal](decisions/architecture.md#d-44-four-bundled-themes--clide-midnight-paper-terminal) — _architecture_
|
||||
- [D-45: Syntax highlighting tokens in the theme pipeline](decisions/architecture.md#d-45-syntax-highlighting-tokens-in-the-theme-pipeline) — _architecture_
|
||||
- [D-46: Core frame builtins vs shipped extensions boundary](decisions/extensions.md#d-46-core-frame-builtins-vs-shipped-extensions-boundary) — _extensions_
|
||||
- [D-47: Interaction model — Claude-is-home layout](decisions/architecture.md#d-47-interaction-model--claude-is-home-layout) — _architecture_
|
||||
- [D-48: Chrome budget — no tabs, no breadcrumbs, keyboard-first](decisions/architecture.md#d-48-chrome-budget--no-tabs-no-breadcrumbs-keyboard-first) — _architecture_
|
||||
- [D-49: Editor mode — inline above Claude, viewer swap](decisions/architecture.md#d-49-editor-mode--inline-above-claude-viewer-swap) — _architecture_
|
||||
- [D-50: Context auto-behavior — right panel reacts to Claude](decisions/architecture.md#d-50-context-auto-behavior--right-panel-reacts-to-claude) — _architecture_
|
||||
- [D-51: Panel collapse — 12px spine with badge](decisions/architecture.md#d-51-panel-collapse--12px-spine-with-badge) — _architecture_
|
||||
- [D-52: Focus mode — full-window takeover](decisions/architecture.md#d-52-focus-mode--full-window-takeover) — _architecture_
|
||||
- [D-53: State persistence across sessions](decisions/architecture.md#d-53-state-persistence-across-sessions) — _architecture_
|
||||
- [D-54: Keyboard map — canonical shortcuts](decisions/architecture.md#d-54-keyboard-map--canonical-shortcuts) — _architecture_
|
||||
- [D-55: Claude pane internal tabs for multi-session](decisions/architecture.md#d-55-claude-pane-internal-tabs-for-multi-session) — _architecture_
|
||||
- [D-56: Dissolve daemon process; Flutter app hosts IPC server](decisions/architecture.md#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server) — _architecture_
|
||||
- [D-57: Frameless custom chrome with per-column 24px hats](decisions/architecture.md#d-57-frameless-custom-chrome-with-per-column-24px-hats) — _architecture_
|
||||
- [D-58: Format engines are adoptable dependencies](decisions/tooling.md#d-58-format-engines-are-adoptable-dependencies) — _tooling_
|
||||
- [D-59: Bundled git via dugite-native](decisions/tooling.md#d-59-bundled-git-via-dugite-native) — _tooling_
|
||||
- [D-60: No network on default launch path](decisions/tooling.md#d-60-no-network-on-default-launch-path) — _tooling_
|
||||
- [D-61: Dependency vetting checklist](decisions/tooling.md#d-61-dependency-vetting-checklist) — _tooling_
|
||||
- [D-62: Dependency removal process](decisions/tooling.md#d-62-dependency-removal-process) — _tooling_
|
||||
- [D-63: Vendored binary rebuild process](decisions/tooling.md#d-63-vendored-binary-rebuild-process) — _tooling_
|
||||
- [D-64: No telemetry — architectural commitment](decisions/architecture.md#d-64-no-telemetry--architectural-commitment) — _architecture_
|
||||
- [D-65: License compatibility matrix](decisions/tooling.md#d-65-license-compatibility-matrix) — _tooling_
|
||||
- [D-66: Line coverage gate at 95%, ratcheted from current](decisions/testing.md#d-66-line-coverage-gate-at-95-ratcheted-from-current) — _testing_
|
||||
- [D-67: Pql changelog files are committed alongside code](decisions/process.md#d-67-pql-changelog-files-are-committed-alongside-code) — _process_
|
||||
|
||||
## Open questions
|
||||
|
||||
- [Q-1: Authorisation granularity on the IPC socket](questions/architecture.md#q-1-authorisation-granularity-on-the-ipc-socket) — _architecture_
|
||||
- [Q-2: Back-pressure on event streams](questions/architecture.md#q-2-back-pressure-on-event-streams) — _architecture_
|
||||
- [Q-3: Event persistence + audit/undo](questions/architecture.md#q-3-event-persistence--auditundo) — _architecture_
|
||||
- [Q-4: `.canvas` schema compatibility with Obsidian](questions/architecture.md#q-4-canvas-schema-compatibility-with-obsidian) — _architecture_
|
||||
- [Q-5: IPC wire-format stability + `schema_version:`](questions/architecture.md#q-5-ipc-wire-format-stability--schema-version) — _architecture_
|
||||
- [Q-6: Window chrome — native frame vs frameless custom](questions/architecture.md#q-6-window-chrome--native-frame-vs-frameless-custom) — _architecture_
|
||||
- [Q-7: macOS app bundle signing / notarisation](questions/architecture.md#q-7-macos-app-bundle-signing--notarisation) — _architecture_
|
||||
- [Q-8: Extension API shape — widgets, subcommands, both?](questions/extensions.md#q-8-extension-api-shape--widgets-subcommands-both) — _extensions_
|
||||
- [Q-9: Lua runtime vendoring](questions/extensions.md#q-9-lua-runtime-vendoring) — _extensions_
|
||||
- [Q-10: Extension manifest `schema_version:`](questions/extensions.md#q-10-extension-manifest-schema-version) — _extensions_
|
||||
- [Q-11: Coverage gates — hard thresholds vs soft reporting](questions/testing.md#q-11-coverage-gates--hard-thresholds-vs-soft-reporting) — _testing_
|
||||
- [Q-12: Screen-reader automation (axe-core via Playwright)](questions/testing.md#q-12-screen-reader-automation-axe-core-via-playwright) — _testing_
|
||||
- [Q-13: Web production-mode a11y](questions/accessibility.md#q-13-web-production-mode-a11y) — _accessibility_
|
||||
- [Q-14: i18n plurals / gender / date-format tooling](questions/accessibility.md#q-14-i18n-plurals--gender--date-format-tooling) — _accessibility_
|
||||
- [Q-15: Editor tab — full LSP vs tree-sitter-only highlight](questions/process.md#q-15-editor-tab--full-lsp-vs-tree-sitter-only-highlight) — _process_
|
||||
- [Q-16: `tree-sitter-dart` grammar maintenance](questions/process.md#q-16-tree-sitter-dart-grammar-maintenance) — _process_
|
||||
- [Q-17: Icon set growth](questions/process.md#q-17-icon-set-growth) — _process_
|
||||
- [Q-18: Theme hot-reload in release builds](questions/process.md#q-18-theme-hot-reload-in-release-builds) — _process_
|
||||
- [Q-19: (withdrawn)](questions/process.md#q-19-withdrawn) — _process_
|
||||
- [Q-20: Kernel DB service — namespaced SQL access?](questions/process.md#q-20-kernel-db-service--namespaced-sql-access) — _process_
|
||||
- [Q-21: Pql absorbs planning vs keeps separate](questions/architecture.md#q-21-pql-absorbs-planning-vs-keeps-separate) — _architecture_
|
||||
- [Q-22: Ticket persistence strategy](questions/architecture.md#q-22-ticket-persistence-strategy) — _architecture_
|
||||
- [Q-23: SSH-remote development — run clide against a remote workspace](questions/architecture.md#q-23-ssh-remote-development--run-clide-against-a-remote-workspace) — _architecture_
|
||||
- [Q-25: Body text face — mono everywhere vs Josefin Sans UI + mono code](questions/architecture.md#q-25-body-text-face--mono-everywhere-vs-josefin-sans-ui--mono-code) — _architecture_
|
||||
- [Q-26: Small screen layout (< 1000px)](questions/architecture.md#q-26-small-screen-layout--1000px) — _architecture_
|
||||
- [Q-27: Two-editor split](questions/architecture.md#q-27-two-editor-split) — _architecture_
|
||||
- [Q-28: Terminal strip scope — shell only or logs/errors/tests](questions/architecture.md#q-28-terminal-strip-scope--shell-only-or-logserrorstests) — _architecture_
|
||||
- [Q-29: Branch picker location](questions/architecture.md#q-29-branch-picker-location) — _architecture_
|
||||
- [Q-30: Focus behavior when editor is dirty and viewer is peeked](questions/architecture.md#q-30-focus-behavior-when-editor-is-dirty-and-viewer-is-peeked) — _architecture_
|
||||
- [Q-31: XWayland fallback for frameless — proper Wayland protocol needed](questions/architecture.md#q-31-xwayland-fallback-for-frameless--proper-wayland-protocol-needed) — _architecture_
|
||||
|
||||
## Rejected
|
||||
|
||||
- [R-2: Go sidecar](rejected/architecture.md#r-2-go-sidecar) — _architecture_
|
||||
- [R-3: `MaterialApp` root](rejected/architecture.md#r-3-materialapp-root) — _architecture_
|
||||
- [R-4: Flutter `intl` + ARB codegen for i18n](rejected/accessibility.md#r-4-flutter-intl--arb-codegen-for-i18n) — _accessibility_
|
||||
- [R-5: Patrol test runner](rejected/testing.md#r-5-patrol-test-runner) — _testing_
|
||||
- [R-6: Nerd-font glyph icons](rejected/process.md#r-6-nerd-font-glyph-icons) — _process_
|
||||
- [R-7: `CupertinoApp` root](rejected/architecture.md#r-7-cupertinoapp-root) — _architecture_
|
||||
- [R-8: Riverpod / Provider / BLoC for state](rejected/architecture.md#r-8-riverpod--provider--bloc-for-state) — _architecture_
|
||||
- [R-9: Port planning tooling into clide](rejected/process.md#r-9-port-planning-tooling-into-clide) — _process_
|
||||
- [R-10: Python-script stopgap under `tooling/db/`](rejected/process.md#r-10-python-script-stopgap-under-toolingdb) — _process_
|
||||
- [R-11: Permanent stopgap](rejected/process.md#r-11-permanent-stopgap) — _process_
|
||||
- [R-12: MaterialApp wrapper from design handoff](rejected/architecture.md#r-12-materialapp-wrapper-from-design-handoff) — _architecture_
|
||||
@@ -8,7 +8,7 @@ A11y + i18n are Tier-0 contracts, not Tier-6 polish.
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Every widget primitive wraps its interaction surface in a `Semantics` node at the point of creation. A11y coverage is a test-time gate (`ci/test_a11y.sh`), not a post-hoc polish pass. `ensureSemantics()` fires at app boot; Flutter's semantics tree is always populated.
|
||||
- **Rationale:** Retrofitting a11y onto a grown UI is what every project that skips this promises to do later and then doesn't. Making it a Tier-0 contract costs one `Semantics` line per primitive and a semantic-coverage test; postponing costs a rewrite.
|
||||
- **Cost:** Widget authors maintain correct labels; tests reject new primitives without semantics. Enforced by `app/test/a11y/` coverage tests.
|
||||
- **Cost:** Widget authors maintain correct labels; tests reject new primitives without semantics. Enforced by `test/a11y/` coverage tests.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
|
||||
### D-21: i18n is a Tier-0 contract (fframe pattern + locale-fallback chain)
|
||||
@@ -13,7 +13,7 @@ Core, rendering, IPC, kernel, panel manager.
|
||||
|
||||
### D-8: Feature-first folder layout
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Under `app/lib/`, organise by feature (`kernel/`, `extension/`, `widgets/`, `builtin/<name>/`) rather than by layer (`models/`, `views/`, `controllers/`). Private implementation lives under each feature's `src/`; the feature's public surface is a barrel file at the feature root (e.g. `app/lib/kernel/kernel.dart`).
|
||||
- **Decision:** Under `lib/`, organise by feature (`kernel/`, `extension/`, `widgets/`, `builtin/<name>/`) rather than by layer (`models/`, `views/`, `controllers/`). Private implementation lives under each feature's `src/`; the feature's public surface is a barrel file at the feature root (e.g. `lib/kernel/kernel.dart`).
|
||||
- **Rationale:** Features grow and get deleted as units; layer-first layouts fragment a feature across three directories and make deletions risky. Matches extensions-as-features (every extension already has its own folder).
|
||||
- **Cost:** Imports cross features only via the barrel — enforce by review, no automated check yet.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
@@ -89,7 +89,7 @@ Core, rendering, IPC, kernel, panel manager.
|
||||
|
||||
### D-4: Ignore file strategy
|
||||
- **Date:** 2026-04-20 (was ADR 0004; ported from the claudian lineage)
|
||||
- **Decision:** One mechanism everywhere: the `ignore_files:` list in `.pql/config.yaml`. Ordered list of gitignore-shaped files; later entries win on per-pattern conflicts. pql defaults to `ignore_files: [.gitignore]`. Per [D-3](#d-3-pql-as-supporter-tool), clide writes the list on load — `[.gitignore, .clideignore]` if `.clideignore` exists, else `[.gitignore]`. `.clideignore` carries **only** the clide-specific deviations from `.gitignore` (supports `!pattern` negations); never duplicate gitignore's contents. Walker magic: none except `.git/` — every other tool-owned dir (`.pql/`, `.clide/`) is added to `.gitignore` at install time; exclusion flows through the normal `ignore_files:` chain.
|
||||
- **Decision:** One mechanism everywhere: the `ignore_files:` list in `.pql/config.yaml`. Ordered list of gitignore-shaped files; later entries win on per-pattern conflicts. pql defaults to `ignore_files: [.gitignore]`. Per [D-3](#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates), clide writes the list on load — `[.gitignore, .clideignore]` if `.clideignore` exists, else `[.gitignore]`. `.clideignore` carries **only** the clide-specific deviations from `.gitignore` (supports `!pattern` negations); never duplicate gitignore's contents. Walker magic: none except `.git/` — every other tool-owned dir (`.pql/`, `.clide/`) is added to `.gitignore` at install time; exclusion flows through the normal `ignore_files:` chain.
|
||||
- **Context:** Every file-enumerating surface in clide (pql query panels, canvas drivers, graph view, file watchers, pane lists, file tree) needs to skip the obvious junk — `vendor/`, `node_modules/`, `dist/`, build artifacts — or results drown in noise. Clide's working assumption is that the git repo *is* the workspace — no separate "vault" concept.
|
||||
- **Rationale:** Users get one config knob, in a file they might already know (pql users) or never need to touch (clide-only users). `.clideignore` is short by design — it's deltas, not a full list. Sidecar consumers read the same key and apply identical precedence, so Claude and the user always see the same filtered surface.
|
||||
- **Cost:** Removing clide from a repo leaves pql working with vanilla defaults (clide's last-written `ignore_files:` stays until pql or the user rewrites it; worth reconsidering during uninstall design).
|
||||
@@ -97,7 +97,8 @@ Core, rendering, IPC, kernel, panel manager.
|
||||
|
||||
### D-5: Dart core; sidecar dissolved; `ptyc` as pql-peer
|
||||
- **Date:** 2026-04-20 (was ADR 0005; supersedes [R-2](rejected.md#r-2-go-sidecar))
|
||||
- **Amendment (2026-04-23):** The separate daemon process and two-package layout are dissolved per [D-56](#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server). Dart-core and ptyc-as-peer principles survive; the daemon binary does not.
|
||||
- **Amendment (2026-04-23):** The separate daemon process and two-package layout are dissolved per [D-56](#d-56-dissolve-daemon-process-flutter-app-hosts-ipc-server). Dart-core principle survives; the daemon binary does not.
|
||||
- **Amendment (2026-05-07):** `ptyc` retired. PTY spawning moved to Dart FFI `forkpty()` (`lib/src/pty/native_pty.dart`). The `ptyc/` source tree, `PtySession`, and `scm_rights.dart` are removed. `pql` remains the sole external supporter tool.
|
||||
- **Decision:** Three moves. **(1) Dart is the core language.** Everything that used to live under `sidecar/` — IPC server, CLI dispatch, process management, file watching, git shell-outs, pql wrapper — is written in Dart. Two execution modes of one Dart AOT binary: `clide <subcommand>` (one-shot, pql-style) and `clide --daemon` (long-running, owns PTYs and subprocesses, survives app restarts). The Flutter app imports the Dart core as a library *and* connects to the daemon over IPC. **(2) The sidecar directory dissolves.** Layout is `app/` (Flutter UI), `lib/` (Dart core), `bin/clide.dart` (AOT entry), `ptyc/` (C helper), no `sidecar/`, no Go module. **(3) `ptyc` is a pql-peer supporter tool.** Small C binary that does `posix_openpt` + `fork` + `exec` + fd-passing via `SCM_RIGHTS`; clide wraps it the same way it wraps pql. Shells out for every PTY (terminal pane, tmux session, Claude, LSP server, debug adapter — one code path). Consumers other than clide can use `ptyc` standalone.
|
||||
- **Context:** [R-2](rejected.md#r-2-go-sidecar) picked Go for the sidecar/CLI on two premises: (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so the muscle memory transfers. On reassessment, both premises broke: the "heavy work" is I/O-bound glue that `dart:io` covers cleanly — the real choice was **separate process vs shared language**, and separate-process is what matters. PTY is the one place Dart is genuinely weak (multi-threaded VM can't safely `fork()`), and once you accept a small native helper, *nothing else* needs to be in the same language.
|
||||
- **Rationale:** One toolchain for the IDE proper (Flutter + Dart). C toolchain needed only to build `ptyc` — tiny, rarely-changing. Session persistence stays because PTY master fds live in the Dart daemon process, not the app. `ptyc` naming: **p** for *project* (parallel to pql's *project query language*), **ptyc** reads as both "PTY + child" (domain vocabulary) and "PTY + C" (implementation language). Usable from Dart, Python, Go, shell — anywhere a subprocess can be spawned and a fd received.
|
||||
@@ -235,4 +236,12 @@ Core, rendering, IPC, kernel, panel manager.
|
||||
- **Cross-reference:** [D-47](#d-47-interaction-model-claude-is-home-layout) (center hat always visible), [D-51](#d-51-panel-collapse-12px-spine-with-badge) (spine-cap behavior).
|
||||
- **Raised by:** 2026-04-23 interaction model refinement.
|
||||
|
||||
### D-64: No telemetry — architectural commitment
|
||||
- **Date:** 2026-05-03
|
||||
- **Decision:** clide does not phone home. No analytics SDKs (Firebase, Sentry, Mixpanel, hand-rolled). No crash reporters that upload automatically — crashes produce local logs the user can read and optionally attach to a manual bug report. No auto-update checks without user action. No license validation calls. No feature flags fetched from a server. No A/B testing, experiments, remote config, or "anonymous usage statistics." This is not a "default off" setting; it is an architectural commitment. Proposals to add telemetry under any framing — opt-in, anonymized, debug-only, "just errors" — are out of scope for this project, full stop.
|
||||
- **Rationale:** clide is a space to think, not a surface for data collection. Users installing clide are choosing a tool that does not watch them. That promise is worth more than any data we could collect. The architectural commitment is the feature.
|
||||
- **Cost:** No usage data for product decisions; no automated crash triage. Accepted — user trust is the product decision.
|
||||
- **Cross-reference:** [D-60](tooling.md#d-60-no-network-on-default-launch-path), `POLICY.md`.
|
||||
- **Raised by:** 2026-05-03 policy-to-decision migration (T-28).
|
||||
|
||||
---
|
||||
@@ -13,7 +13,7 @@ Extension contract, Lua runtime, grain, contribution points.
|
||||
|
||||
### D-16: Built-ins in Dart, third-party in sandboxed Lua
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Bundled extensions (every `app/lib/builtin/<name>`) are Dart — they link into the app binary. Third-party extensions (Tier 6) run in sandboxed Lua via the `ptyc`-peer Lua runtime (see [D-19](#d-19-lua-runtime-as-ptyc-peer-supporter-tool)). The contribution contract is language-agnostic — same contribution shapes, same manifest schema.
|
||||
- **Decision:** Bundled extensions (every `lib/builtin/<name>`) are Dart — they link into the app binary. Third-party extensions (Tier 6) run in sandboxed Lua via the `ptyc`-peer Lua runtime (see [D-19](#d-19-lua-runtime-as-ptyc-peer-supporter-tool)). The contribution contract is language-agnostic — same contribution shapes, same manifest schema.
|
||||
- **Rationale:** Dart built-ins get full SDK power (custom painters, isolates, FFI); third-party Lua gets a narrow capability API, no arbitrary syscalls, no deps on pub.dev. VS Code's Node-runs-with-full-power model is a supply-chain nightmare we're explicitly rejecting.
|
||||
- **Cost:** Two implementation paths for the same contract; we pay in API design to keep them equivalent at the seams.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
@@ -41,7 +41,7 @@ Extension contract, Lua runtime, grain, contribution points.
|
||||
|
||||
### D-46: Core frame builtins vs shipped extensions boundary
|
||||
- **Date:** 2026-04-22
|
||||
- **Decision:** The `app/lib/builtin/` directory is reserved for core frame infrastructure — components the shell cannot function without. Everything that renders *content* (editor surfaces, tool panels, integrations) is a shipped extension: still Dart, still bundled in the binary, but architecturally an extension that registers through the contribution contract and could in principle be disabled by the user.
|
||||
- **Decision:** The `lib/builtin/` directory is reserved for core frame infrastructure — components the shell cannot function without. Everything that renders *content* (editor surfaces, tool panels, integrations) is a shipped extension: still Dart, still bundled in the binary, but architecturally an extension that registers through the contribution contract and could in principle be disabled by the user.
|
||||
|
||||
**Core frame builtins** (cannot be disabled; the frame breaks without them):
|
||||
`default-layout`, `welcome`, `ipc-status`, `theme-picker`, `terminal`, `files`, `grammars-core`, `settings-ui`, `extensions-ui`, `keybindings-ui`.
|
||||
@@ -53,7 +53,7 @@ Extension contract, Lua runtime, grain, contribution points.
|
||||
`editor`, `claude`, `claude-control`, `markdown`, `diff`, `git-ui`, `pql`, `canvas`, `graph`, `decisions`, `tickets`, `todos`, `problems`.
|
||||
|
||||
- **Rationale:** The previous session bled several content extensions (jira, todos, decisions, tickets, canvas, graph) into `builtin/` as stubs, treating "shipped with the app" as "part of the frame." This conflates two concerns: the frame's structural integrity and the bundled feature set. A user who disables the canvas extension should get a working IDE with no canvas panel; a user who disables the layout extension gets a broken window. The boundary is: can the frame render and function without it? If yes, it's a shipped extension, not a frame builtin.
|
||||
- **Cost:** Shipped extensions need a separate registration path (e.g. `app/lib/extensions/` or equivalent) distinct from `app/lib/builtin/`. The extension contract must support "bundled Dart extension" as a first-class category alongside "builtin" and "third-party Lua." Migration is incremental — move one at a time, each behind a working build.
|
||||
- **Cost:** Shipped extensions need a separate registration path (e.g. `lib/extensions/` or equivalent) distinct from `lib/builtin/`. The extension contract must support "bundled Dart extension" as a first-class category alongside "builtin" and "third-party Lua." Migration is incremental — move one at a time, each behind a working build.
|
||||
- **Supersedes:** Removes `builtin.jira` (already deleted; should never have been a builtin — Jira integration is a third-party extension, not a shipped one).
|
||||
- **Raised by:** 2026-04-22 session review.
|
||||
|
||||
@@ -43,7 +43,7 @@ Q&D record system itself, kanban, commit conventions, changelog.
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Planning subcommands (`decisions`, `ticket`, `plan`) land in pql's repo long-term. Clide consumes them via shell-out, matching [D-3](architecture.md)'s wrap-don't-duplicate rule for pql. Clide does not grow Dart subcommands for planning.
|
||||
- **Rationale:** A terminal user or a user in VS Code / JetBrains still needs Q&D access. Binding planning tooling to clide-the-Flutter-app would cut them off from their own work — see [R-9](rejected.md#r-9-port-planning-tooling-into-clide). pql is already the CLI, already universal, already wrapped by clide.
|
||||
- **Cost:** Planning features don't ship until pql catches up. Mitigated by [D-40](#d-40-python-stopgap-under-toolsscriptsplan). Gated by [Q-21](questions-process.md#q-21-pql-absorbs-planning-vs-keeps-separate).
|
||||
- **Cost:** Planning features don't ship until pql catches up. Mitigated by [D-40](#d-40-superseded-python-stopgap-under-toolsscriptsplan). Gated by [Q-21](questions-process.md#q-21-pql-absorbs-planning-vs-keeps-separate).
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
|
||||
### D-40: [SUPERSEDED] Python stopgap under `tools/scripts/plan`
|
||||
@@ -53,6 +53,15 @@ Q&D record system itself, kanban, commit conventions, changelog.
|
||||
- **Rationale:** Planning tooling must work day one. Pql's Go implementation won't land for at least a cycle or two. Without a stopgap, the convention lives on paper; with one, tickets + decisions are queryable from today. Same schema means migration is call-site find-replace (`tools/scripts/plan ` → `pql `), no data migration.
|
||||
- **Cost:** Python dep on contributors' machines (already present on most Linux dists). One time-limited tool to maintain. See [R-10](rejected.md#r-10-python-script-stopgap-at-toolingdb) for why `tools/scripts/plan` and not `tooling/db/`.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
- **Amendment (2026-04-22):** Sunset condition met. pql 1.0.0 ships full feature parity. Stopgap deleted per [R-11](rejected.md#r-11-permanent-stopgap).
|
||||
- **Amendment (2026-04-22):** Sunset condition met. pql 1.0.0 ships full feature parity. Stopgap deleted per [R-11](../rejected/process.md#r-11-permanent-stopgap).
|
||||
|
||||
### D-67: Pql changelog files are committed alongside code
|
||||
- **Date:** 2026-05-11
|
||||
- **Decision:** Clide commits `.pql/changelog/{tickets,ticket_history,ticket_deps,ticket_labels}/<YYYY-MM>.sql` files alongside source changes. `pql.db` itself stays gitignored — it's the local replay target, rebuildable from changelog + `governance/*.md` on any clone. Pre-commit hook auto-stages the changelog deltas; post-merge / post-checkout / post-rewrite hooks replay them into `pql.db`.
|
||||
- **Rationale:** Resolves [Q-22](../questions/architecture.md#q-22-ticket-persistence-strategy). The single-file `pql-plan.json` snapshot model couldn't merge concurrent edits cleanly (every ticket flip rewrote the same JSON). Pql 1.4.x reshaped persistence into append-only per-month SQL files with inline LWW guards, which is option (3) of Q-22 (markdown/SQL mirror, git-legible, DB rebuildable) evolved into a form that merges by default. Clide migrated to it on 2026-05-09 (`01a99ed`, `d162ba2`).
|
||||
- **Cost:** Each user-visible commit also carries the matching changelog diff. The auto-stage hook handles it. Changelog files grow monotonically across commits even on no-change exports — minor file-size cost, no replay-correctness impact (LWW dedupes on import).
|
||||
- **Resolves:** [Q-22](../questions/architecture.md#q-22-ticket-persistence-strategy).
|
||||
- **Cross-references:** [D-3](architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates), [D-39](#d-39-planning-tooling-lives-in-pql-not-clide).
|
||||
- **Raised by:** 2026-05-11; cleanup after pql D-21 / governance/ migration.
|
||||
|
||||
---
|
||||
@@ -6,7 +6,7 @@ Test pyramid, drivers, client-side constraint.
|
||||
|
||||
### D-23: Test pyramid — seven layers
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** The pyramid has seven layers: unit (pure Dart) → widget (pumped + find) → golden (visual primitives) → a11y (semantics coverage + keyboard + contrast + i18n) → integration (`flutter test integration_test/`) → E2E (Playwright driving the WASM build + `clide --daemon` subprocess) → startup-smoke (`ci/smoke_bundle.sh`: build Linux release, run under xvfb for 5 s).
|
||||
- **Decision:** The pyramid has seven layers: unit (pure Dart) → widget (pumped + find) → golden (visual primitives) → a11y (semantics coverage + keyboard + contrast + i18n) → integration (`flutter test integration_test/`) → E2E (Playwright driving the WASM build) → startup-smoke (`ci/smoke_bundle.sh`: build Linux release, run under xvfb for 5 s).
|
||||
- **Rationale:** Each layer catches a distinct regression class. Skipping any layer means that class ships unprotected. Pushed back when earlier rounds proposed "just widget + E2E"; widget can't catch paint regressions (that's golden), E2E can't catch a11y tree drift (that's semantics).
|
||||
- **Cost:** Seven CI jobs; total wall time budgeted at < 15 min. Pre-push runs layers 1-4 (< 90 s — see [D-29](#d-29-pre-push-gate-fast-layer-only)).
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
@@ -41,7 +41,7 @@ Test pyramid, drivers, client-side constraint.
|
||||
|
||||
### D-28: Test organisation — mirror `lib/` in `test/`
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Every test file lives at the same relative path as its subject. `app/lib/kernel/src/i18n/catalog_loader.dart` pairs with `app/test/kernel/i18n/catalog_loader_test.dart`. No separate `unit/` vs `widget/` directories; test type is detected by what the test imports.
|
||||
- **Decision:** Every test file lives at the same relative path as its subject. `lib/kernel/src/i18n/catalog_loader.dart` pairs with `test/kernel/i18n/catalog_loader_test.dart`. No separate `unit/` vs `widget/` directories; test type is detected by what the test imports.
|
||||
- **Rationale:** Matching paths makes "jump to test" predictable in any editor. Type-by-imports matches how `flutter test` already works.
|
||||
- **Cost:** Large feature folders mirror into large test folders. Acceptable.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
@@ -60,4 +60,12 @@ Test pyramid, drivers, client-side constraint.
|
||||
- **Cost:** pql / daemon / extension tests stand up real subprocesses and real sockets locally — no mocked network convenience.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
|
||||
### D-66: Line coverage gate at 95%, ratcheted from current
|
||||
- **Date:** 2026-05-06
|
||||
- **Decision:** The pre-push gate runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info`, and hard-fails if total line coverage drops below a committed floor at `coverage/floor.txt`. The floor starts at the actual current coverage (≈35%, dragged down by `lib/src/terminal/`'s 0.4%) and only ever ratchets up. The end target is 95%; getting there is tracked as a campaign of deliberate floor bumps under one epic ticket. **No carve-outs** — code under `lib/` is owned regardless of file-header attribution, including the terminal emulator port. Branch coverage is not gated (Dart's lcov output models it weakly). Lint suppressions to dodge the gate are never acceptable.
|
||||
- **Rationale:** A flat 95% threshold today blocks every push; an informational coverage report rots into noise. The committed-floor ratchet makes "don't make it worse" the durable rule and turns the journey to 95% into explicit, reviewed bumps rather than a single overnight cliff. Excluding `forkpty`-tagged tests matches `ci/test.sh` (forkpty + flutter test runner are incompatible — see `test/pty/session_test.dart`).
|
||||
- **Cost:** Pre-push wall time grows by `flutter test --coverage` (currently ≈11 s on this tree). Acceptable within D-29's < 90 s budget; reassess if it slips. Floor bumps require an explicit edit to `coverage/floor.txt` in the same commit that adds tests — so contributors can't silently raise it.
|
||||
- **Cross-reference:** [D-29](#d-29-pre-push-gate-fast-layer-only).
|
||||
- **Raised by:** 2026-05-06 — coverage triage during T-73 follow-up.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,94 @@
|
||||
# Tooling Decisions
|
||||
|
||||
Toolchain, supply chain, CI, ignore strategy.
|
||||
|
||||
---
|
||||
|
||||
### D-31: Prefer-zero-deps, exact-pin
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** Default to writing code ourselves. Every third-party Dart dependency needs a paragraph of justification in the PR that adds it. What stays is exact-pinned in `pubspec.yaml` (no caret ranges), `pubspec.lock` is committed, and advisories are reviewed before every bump.
|
||||
- **Rationale:** Supply-chain gate. Flutter SDK + Dart SDK give us most of what we need; the dependencies we keep are the ones we can't reasonably write (yaml parser, mocktail, alchemist). Exact-pin because caret ranges mean "the CVE bumps itself in silently."
|
||||
- **Cost:** Longer PR descriptions for deps; occasional reinvention of a convenience. Accepted.
|
||||
- **Raised by:** 2026-04-21 planning; reinforced by user feedback memory.
|
||||
|
||||
### D-32: CI — Gitea primary, Linux-only runners, not yet activated
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** CI config lives at `.gitea/workflows/test.yml` (Gitea Actions consumes GitHub-Actions syntax). Runners are Linux only; macOS is tested locally. The workflow is ready but Gitea Actions is not yet activated on the instance — the file is a staged pipeline for review. If the repo moves to GitHub, the file copies to `.github/workflows/test.yml` verbatim.
|
||||
- **Rationale:** We want the CI story defined before we turn CI on — lower blast radius on early red builds. GitHub portability is free because the syntax is shared.
|
||||
- **Cost:** PRs don't run CI yet; `make push-check` is the gate until activation.
|
||||
- **Raised by:** 2026-04-21 planning.
|
||||
|
||||
### D-42: Dependencies documented in `licenses.yaml`
|
||||
- **Date:** 2026-04-22
|
||||
- **Decision:** `assets/licenses.yaml` has three sections: `self:` (clide's MIT license, rendered first in the About screen so the user knows what they're running), `dependencies:` (third-party artefacts that **ship in the binary** — fonts, runtime Dart packages, native supporter tools, bundled data), and `dev_dependencies:` (build-time-only tooling — test runners, mocks, lints, golden harness — tracked for audit but **not rendered** in the About screen because they don't reach the user). Each entry has name, kind, version, homepage, license identifier, and a one-line purpose; runtime entries also carry a `license_file:` pointer to the bundled license text so the About screen can display it verbatim. Adding any dependency is a two-step commit: add the artefact **and** the corresponding `licenses.yaml` entry in the same changeset, under the correct section.
|
||||
- **Rationale:** Complements [D-31](#d-31-prefer-zero-deps-exact-pin). Prefer-zero-deps is a *budget*; `licenses.yaml` is the *visible consequence*. An extra row in the About screen is a review-time signal that the shipped-binary surface grew. Splitting dev deps out keeps the user-facing list small and honest — a test framework is not something the user needs to see in About — while still documenting every supply-chain input for audit completeness. The runtime entries discharge the redistribution obligations bundled licenses impose (OFL, MIT, BSD all require preserving the license text alongside the binary) without ad-hoc NOTICE files.
|
||||
- **Cost:** One extra edit per dep. Zero tolerance for drift — an un-listed dep is a contributor-visible bug. Until the About screen lands at Tier 6, `licenses.yaml` is accurate but not rendered; the discipline applies from now regardless so Tier 6 inherits a clean list.
|
||||
- **Raised by:** 2026-04-22 planning (user-directed best practice).
|
||||
|
||||
### D-33: Golden-output ignore pattern — `coverage.*` excludes output, not scripts
|
||||
- **Date:** 2026-04-21
|
||||
- **Decision:** `.gitignore` excludes `coverage.*` (the lcov output files from `flutter test --coverage`). Coverage-related scripts are named `ci/test_coverage.sh` (not `ci/coverage.sh`) to stay outside the pattern.
|
||||
- **Rationale:** An earlier draft named the script `ci/coverage.sh` and it was silently git-ignored. Renaming the script is cheaper than narrowing the gitignore pattern (which risks re-introducing output churn).
|
||||
- **Cost:** Script names have a convention to follow.
|
||||
- **Raised by:** 2026-04-21 planning (caught during commit rehearsal).
|
||||
|
||||
### D-58: Format engines are adoptable dependencies
|
||||
- **Date:** 2026-04-23
|
||||
- **Decision:** The "own the rendering stack" guardrail applies to **UI chrome** — panels, tabs, panes, canvas, terminal, layout primitives. **Format engines** — packages that parse or render external file formats (SVG, markdown, HTML, terminal escape sequences, tree-sitter grammars) — are adoptable like any other dependency: vet, exact-pin, CVE-lock, document in `licenses.yaml`. They are not shortcuts for lazy coding; they are well-maintained renderers for formats we didn't invent. The distinction: if it renders *our* UI, we own it; if it renders *someone else's file format*, we adopt a parser/renderer and sandbox it.
|
||||
- **Adopted under this rule:** `jovial_svg` (SVG renderer), `markdown` (MD parser; renderer is ours), `flutter_widget_from_html_core` (HTML renderer; sandboxed), `xterm` (terminal emulator), tree-sitter (syntax highlighting). Canvas (`CustomPaint` + `InteractiveViewer`) stays in-house — UI chrome, not a format engine.
|
||||
- **Amendment to D-31 (prefer-zero-deps):** D-31's "prefer-zero-deps" still applies — every new dependency needs justification. This record clarifies that format engines clear the justification bar by default. The supply-chain gate (exact-pin, advisory review, `licenses.yaml`) still applies.
|
||||
- **Rationale:** Reimplementing SVG, markdown, or VT100 parsing adds months of work for no fidelity gain. tree-sitter already set this precedent. The key is sandboxing: HTML rendering must whitelist tags/attributes; SVG must not execute scripts; markdown rendering goes through our own widget builder so we control the output.
|
||||
- **Cost:** Each adopted engine adds transitive dependencies and supply-chain surface. Mitigated by exact-pinning and `make security`.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml).
|
||||
- **Raised by:** 2026-04-23 format engine evaluation.
|
||||
|
||||
### D-59: Bundled git via dugite-native
|
||||
- **Date:** 2026-04-25
|
||||
- **Decision:** Ship a self-contained Git binary from [dugite-native](https://github.com/desktop/dugite-native) (the same distribution GitHub Desktop bundles). Downloaded at build time via `make dugite-fetch`, stored under `native/dugite/`, gitignored. The `Toolchain` class resolves to the bundled binary first, falling back to system git on PATH.
|
||||
- **Rationale:** The macOS app sandbox blocks execution of Homebrew-installed git (symlinks resolve to Cellar paths that SBPL cannot match without freezing rendering). `/usr/bin/git` is an xcrun shim that refuses to run inside a sandbox. Bundling dugite-native makes clide self-contained — no dependency on Homebrew, Xcode CLT, or system git. The approach is proven: GitHub Desktop, Tower, and other git GUI apps all bundle their own git for the same reason.
|
||||
- **Alternatives rejected:** (R) libgit2 via FFI — missing porcelain commands (pull/push/rebase), no hooks, would require rewriting GitClient. (R) Build git from source — dugite-native already does this with better infra. (R) SBPL exceptions for Homebrew — `(subpath "/opt/homebrew")` for process-exec freezes Flutter rendering on macOS 26.
|
||||
- **Cost:** ~57 MB download (~199 MB unpacked, stripped at build time). Must track dugite-native releases for security updates (tracked in T-88). GPL-2.0 (git binary) applies to the bundled artefact, not to clide's MIT code.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml).
|
||||
- **Raised by:** 2026-04-25 macOS sandbox investigation.
|
||||
|
||||
### D-60: No network on default launch path
|
||||
- **Date:** 2026-04-26
|
||||
- **Decision:** clide does not perform network I/O during app startup, library initialization, or first use of any API unless the user has explicitly taken an action whose stated purpose is to cause a network fetch. Opening the app, opening a file, or typing in a buffer are not such actions. Libraries that download native binaries on first import (the `wasm_run` pattern), auto-installing language servers/grammars, CDN-fetched assets, startup telemetry, and unsolicited update checks are all prohibited. Signed, pinned fetches are permitted only when: the URL is hardcoded in the repo, the artifact is verified against a committed hash or signature, the fetch is cached, failure produces a clear error, and the primary function works without the fetch succeeding. If all five cannot be satisfied, vendor the artifact or require explicit user action.
|
||||
- **Rationale:** clide's security model claims that app behavior on a user's machine is fully determined by the signed release artifact and the repository state at build time. The moment something is fetched from the network that wasn't audited at build time, the entire sandboxing and trust story collapses. See `POLICY.md` §"The core rule."
|
||||
- **Cost:** Some features require vendoring artifacts that other apps would download at first launch. Accepted — the trust boundary is worth the extra build complexity.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-63](#d-63-vendored-binary-rebuild-process), `POLICY.md`.
|
||||
- **Raised by:** 2026-04-26 policy-to-decision migration (T-28).
|
||||
|
||||
### D-61: Dependency vetting checklist
|
||||
- **Date:** 2026-04-26
|
||||
- **Decision:** Before adding any dependency (direct or transitive), verify: (1) **Network behavior** — no network I/O during import, init, or first call; no postinstall scripts that download binaries; check transitive deps with `flutter pub deps`. (2) **Binary provenance** — native binaries must be built from source in the same repo, not fetched from release artifacts. (3) **Maintainership** — single-maintainer packages need explicit sign-off and a documented fallback; packages with no activity in 12+ months require a controlled fork or inlining. (4) **Surface area** — prefer packages that do one thing; a dep adding 15 transitive deps for a 100-line problem should be inlined. (5) **Version pinning** — exact-pinned per D-31, lockfile committed, CVE-checked, source-reviewed, justified in place. (6) **License** — compatible per D-65.
|
||||
- **Rationale:** D-31 states the budget; this record codifies the gate each dependency must pass. The checklist exists so agents and human contributors apply the same standard without re-deriving it each time.
|
||||
- **Cost:** Longer evaluation cycle for new dependencies. Intentional — the cost of a bad dep is higher.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-60](#d-60-no-network-on-default-launch-path), [D-65](#d-65-license-compatibility-matrix), `POLICY.md`.
|
||||
- **Raised by:** 2026-04-26 policy-to-decision migration (T-28).
|
||||
|
||||
### D-62: Dependency removal process
|
||||
- **Date:** 2026-04-26
|
||||
- **Decision:** A dependency is not removed until all five steps are completed in a single PR: (1) Grep the entire repository for references to the package, its exports, and contributed type names — zero hits outside git history. (2) Regenerate the lockfile. (3) Update `assets/licenses.yaml` to drop the package and any orphaned transitive deps. (4) Remove any vendored artifacts (binaries, prebuilt assets, generated bindings) and delete their `BUILD.md` records. (5) Check for architectural assumptions the dep was carrying — if it justified a data flow, build step, or platform strategy, the replacement must pick up those responsibilities or the relevant D-record must be updated.
|
||||
- **Rationale:** "I deleted the line from pubspec.yaml" is the start of a removal, not the end. Partial removals leave orphaned lockfile entries (installed on fresh clones), stale license entries, or orphaned vendored binaries that look legitimate.
|
||||
- **Cost:** Removal PRs are larger than the one-line diff suggests. Accepted.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml), `POLICY.md`.
|
||||
- **Raised by:** 2026-04-26 policy-to-decision migration (T-28).
|
||||
|
||||
### D-63: Vendored binary rebuild process
|
||||
- **Date:** 2026-04-26
|
||||
- **Decision:** Every vendored native binary has a `BUILD.md` next to it recording: (1) exact upstream source (git URL + commit SHA, not a version tag), (2) full build command with all compile flags, (3) toolchain version (compiler, linker, target triple), (4) expected output size and SHA-256 hash, (5) any patches applied (stored as `.patch` files in the same directory). Rebuilds happen in CI, not on contributor machines. The rebuild PR updates `BUILD.md`, the binaries, and hashes atomically. No binary is committed without a reproducibility record. Security patches to vendored deps are tracked with the same urgency as source-level vulnerabilities. Dropping a platform requires a policy decision; adding one requires adding it to the CI matrix and rebuilding all vendored binaries first.
|
||||
- **Rationale:** Vendored binaries are inside the trust boundary — the signed release contains exactly these bytes. Without reproducibility records, a committed binary is unverifiable and therefore untrustworthy.
|
||||
- **Cost:** Rebuilds require CI infrastructure and cross-compilation. Currently partially manual (T-25 tracks full CI automation).
|
||||
- **Cross-reference:** [D-60](#d-60-no-network-on-default-launch-path), [D-42](#d-42-dependencies-documented-in-licensesyaml), T-25, `POLICY.md`.
|
||||
- **Raised by:** 2026-04-26 policy-to-decision migration (T-28).
|
||||
|
||||
### D-65: License compatibility matrix
|
||||
- **Date:** 2026-04-26
|
||||
- **Decision:** clide is MIT-licensed. Every dependency, vendored binary, bundled font, and asset must be compatible and attributed. **Compatible (permissive):** MIT, Apache-2.0, BSD-2/3, ISC, Zlib, Unlicense, CC0. **Compatible with care (copyleft):** MPL-2.0 for libraries; LGPL only for dynamically-linked vendored binaries where users can replace the library. **Not compatible:** GPL for linked code (GPL vendored binaries like git are fine — they ship as separate executables), AGPL, SSPL, "commercial use prohibited," unreviewed custom licenses. Apache-2.0 deps preserve their NOTICE file verbatim. Apache-2.0-with-LLVM-exception requires the exception text specifically. Fonts and icon sets are attributed even if the license doesn't strictly require it. An incompatible or unclear license is disqualifying regardless of technical merit.
|
||||
- **Rationale:** The compatibility rules existed in POLICY.md but were not captured as a D-record, making them invisible to the decision-reference system. This record makes them queryable and cross-referenceable.
|
||||
- **Cost:** License evaluation adds time to the vetting checklist. Intentional.
|
||||
- **Cross-reference:** [D-31](#d-31-prefer-zero-deps-exact-pin), [D-42](#d-42-dependencies-documented-in-licensesyaml), [D-61](#d-61-dependency-vetting-checklist), `POLICY.md`.
|
||||
- **Raised by:** 2026-04-26 policy-to-decision migration (T-28).
|
||||
|
||||
---
|
||||
@@ -48,9 +48,9 @@ ticket persistence.
|
||||
- **Source:** 2026-04-21 planning.
|
||||
|
||||
### Q-21: Pql absorbs planning vs keeps separate
|
||||
- **Status:** Open
|
||||
- **Question:** Three shapes for planning tooling's long-term home: (A) Pql absorbs planning — `pql decisions …` + `pql ticket …` subcommands; clide shells out. (B) Clide absorbs pql — reverse [D-3](architecture.md), one big Dart tool. (C) Separate new binary just for planning.
|
||||
- **Context:** User is leaning (A). This plan assumes (A) without committing. If (A) doesn't land, [D-40](process.md#d-40-python-stopgap-under-toolsscriptsplan)'s sunset condition changes. Gates all tooling work. Integration constraints that shape this question are captured in [D-39](process.md#d-39-planning-tooling-lives-in-pql) / [R-9](rejected.md#r-9-port-planning-tooling-into-clide).
|
||||
- **Status:** Resolved → [D-3](../decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates) + [D-39](../decisions/process.md#d-39-planning-tooling-lives-in-pql-not-clide)
|
||||
- **Question:** Three shapes for planning tooling's long-term home: (A) Pql absorbs planning — `pql decisions …` + `pql ticket …` subcommands; clide shells out. (B) Clide absorbs pql — reverse [D-3](../decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates), one big Dart tool. (C) Separate new binary just for planning.
|
||||
- **Context:** Resolved 2026-05-11 in favour of (A). pql 1.4.30 ships full planning surface (`pql decisions …`, `pql ticket …`, `pql plan …`). Clide consumes via shell-out under `lib/src/pql/`. D-39 already encoded the intent; D-3 the wrap-don't-duplicate rule. The Python stopgap ([D-40](../decisions/process.md#d-40-superseded-python-stopgap-under-toolsscriptsplan)) was sunset on schedule.
|
||||
- **Source:** 2026-04-21 planning.
|
||||
|
||||
### Q-23: SSH-remote development — run clide against a remote workspace
|
||||
@@ -60,9 +60,9 @@ ticket persistence.
|
||||
- **Source:** 2026-04-22 planning (user-raised).
|
||||
|
||||
### Q-22: Ticket persistence strategy
|
||||
- **Status:** Open
|
||||
- **Status:** Resolved → [D-67](../decisions/process.md#d-67-pql-changelog-files-are-committed-alongside-code)
|
||||
- **Question:** Once [Q-21](#q-21-pql-absorbs-planning-vs-keeps-separate) resolves in favour of (A), how do tickets handle shared team state? (1) Never commit (per-dev, ephemeral — works for solo). (2) Commit on milestone (settled-reach's sprint-close pattern — kanban has no natural equivalent, `release` or `tier-cut` is the closest). (3) Markdown mirror — every mutation writes `tickets/T-NNN.md` alongside SQLite; git-legible authoritative record; DB is rebuildable. (3) is probably the eventual answer.
|
||||
- **Context:** Kanban's lack of a sync event breaks settled-reach's SQLite-authoritative approach the moment two devs collaborate.
|
||||
- **Context:** Resolved 2026-05-11 → option (3), evolved. Pql 1.4.x reshaped ticket persistence into append-only per-month `.pql/changelog/<table>/<YYYY-MM>.sql` files with inline LWW guards. Committed alongside code; `pql.db` rebuildable from changelog + `governance/*.md`. Clide migrated on 2026-05-09.
|
||||
- **Source:** 2026-04-21 planning.
|
||||
|
||||
### Q-25: Body text face — mono everywhere vs Josefin Sans UI + mono code
|
||||
@@ -16,7 +16,7 @@ Tooling-domain questions currently live here too. Split into
|
||||
### Q-16: `tree-sitter-dart` grammar maintenance
|
||||
- **Status:** Open
|
||||
- **Question:** `UserNobody14/tree-sitter-dart` is archived. `nielsenko/tree-sitter-dart` is the maintained fork. Do we pin `nielsenko/`, mirror it in-repo, or lean on the Dart analyzer's own semantic output and skip tree-sitter for Dart?
|
||||
- **Context:** If tree-sitter is the Tier-2 answer ([Q-15](#q-15-editor-tab-full-lsp-vs-tree-sitter-only)), grammar sourcing matters.
|
||||
- **Context:** If tree-sitter is the Tier-2 answer ([Q-15](#q-15-editor-tab-full-lsp-vs-tree-sitter-only-highlight)), grammar sourcing matters.
|
||||
- **Source:** 2026-04-21 planning.
|
||||
|
||||
### Q-17: Icon set growth
|
||||
@@ -0,0 +1,13 @@
|
||||
# Rejected — Accessibility
|
||||
|
||||
Alternatives considered and rejected, with rationale preserved for
|
||||
future reference.
|
||||
|
||||
---
|
||||
|
||||
### R-4: Flutter `intl` + ARB codegen for i18n
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** ARB codegen is inflexible for plugin-contributed catalogs — every catalogue needs a codegen pass, every extension ships with pre-generated Dart, and runtime merging is fighting the tool. The fframe text-driven pattern reads JSON at runtime with no codegen, which fits extension-shipped catalogs cleanly.
|
||||
- **Cross-reference:** [D-21](../decisions/accessibility.md#d-21-i18n-is-a-tier-0-contract)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,33 @@
|
||||
# Rejected — Architecture
|
||||
|
||||
Alternatives considered and rejected, with rationale preserved for
|
||||
future reference.
|
||||
|
||||
---
|
||||
|
||||
### R-2: Go sidecar
|
||||
- **Rejected:** 2026-04-20 (was ADR 0002; superseded by [D-5](../decisions/architecture.md#d-5-dart-core-ptyc-peer))
|
||||
- **Reason:** The ADR picked Go on two premises — (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so muscle memory transfers. Both broke on reassessment. The sidecar stripped of PTY is I/O-bound glue that `dart:io` covers cleanly (unix sockets, JSON-lines framing, process tables, shell-outs). The real axis was *separate process vs shared language*, not Go vs Rust, and separate-process is what matters (session persistence needs the daemon to outlive the app), not language. PTY is the one place Dart is genuinely weak — Dart's multi-threaded VM can't safely `fork()` — and that single constraint forces a native helper regardless, independent of whether the rest of the core is Dart. Once a small native helper is accepted, the question "does *everything else* need to be in that same native language" answers itself: no. Go sidecar directory dissolved; `ptyc` (C, PTY-only, pql-peer) is the surviving native supporter tool.
|
||||
- **Cross-reference:** [D-5](../decisions/architecture.md#d-5-dart-core-ptyc-peer)
|
||||
|
||||
### R-3: `MaterialApp` root
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Dragged in Material theming, default icons, and platform chrome that fought the custom three-tier theme pipeline ([D-9](../decisions/architecture.md#d-9-three-tier-theme-pipeline)). Every bundled theme had to override Material defaults to look like clide; the overrides were visible in widget tests as "why is this `ElevatedButton` colored this way."
|
||||
- **Cross-reference:** [D-7](../decisions/architecture.md#d-7-app-root-is-bare-widgetsapp)
|
||||
|
||||
### R-7: `CupertinoApp` root
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** iOS-opinionated; wrong shell for a Linux-primary desktop IDE. Same theming-collision problem as [R-3](#r-3-materialapp-root).
|
||||
- **Cross-reference:** [D-7](../decisions/architecture.md#d-7-app-root-is-bare-widgetsapp)
|
||||
|
||||
### R-8: Riverpod / Provider / BLoC for state
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Violates [D-31](../decisions/tooling.md#d-31-prefer-zero-deps-exact-pin). `ChangeNotifier` + `ListenableBuilder` ship in the SDK, fake trivially, and cover the state model we need. The ergonomic wins of Riverpod / Provider don't clear the "new dependency" bar at clide's scale.
|
||||
- **Cross-reference:** [D-10](../decisions/architecture.md#d-10-state-management-changenotifier)
|
||||
|
||||
### R-12: MaterialApp wrapper from design handoff
|
||||
- **Rejected:** 2026-04-22
|
||||
- **Reason:** The design handoff delivers theme files as `MaterialApp`/`ThemeData` Dart classes. This is the delivery format of claude.ai/design, not a design intent. Adopting Material's widget system would contradict [D-7](../decisions/architecture.md#d-7-app-root-is-bare-widgetsapp) (bare WidgetsApp, no Material/Cupertino). We translate the palette tokens and syntax roles into our existing YAML + `SurfaceTokens` pipeline.
|
||||
- **Cross-reference:** [D-43](../decisions/architecture.md#d-43-design-handoff-adopt-token-palettes-reject-material-wrapper)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,28 @@
|
||||
# Rejected — Process
|
||||
|
||||
Alternatives considered and rejected, with rationale preserved for
|
||||
future reference.
|
||||
|
||||
---
|
||||
|
||||
### R-6: Nerd-font glyph icons
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** TUI hangover from the Python-era clide under `legacy/`. Not desktop-native; forces a font dependency; doesn't theme consistently. Clide uses custom icon primitives (Tier 6 revisits with proper icon-set design).
|
||||
- **Cross-reference:** [Q-17](../questions/process.md#q-17-icon-set-growth)
|
||||
|
||||
### R-9: Port planning tooling into clide
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Earlier in the planning session the assumption was "clide owns Dart subcommands for decisions + tickets." That breaks the day a contributor works in a terminal or in VS Code / JetBrains — they have no `clide` binary to run. Reversing: pql owns planning long-term (see [D-39](../decisions/process.md#d-39-planning-tooling-lives-in-pql)); clide consumes via shell-out.
|
||||
- **Cross-reference:** [D-39](../decisions/process.md#d-39-planning-tooling-lives-in-pql)
|
||||
|
||||
### R-10: Python-script stopgap under `tooling/db/`
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Location, not language. Settled-reach puts scripts at `tooling/db/` — copying that path here creates a script-pollution problem: every project using the pattern commits its own copy. The accepted Python port ([D-40](../decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)) lives at `tools/scripts/plan`, clearly signalled as dev-tooling and time-limited.
|
||||
- **Cross-reference:** [D-40](../decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||
|
||||
### R-11: Permanent stopgap
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** If the Python port under `tools/scripts/plan` outlasts pql's feature parity, delete it. The deletion commit should be one changeset: remove `tools/scripts/plan`, remove its Makefile target (`decisions-validate` rewires to `pql decisions validate`), add a `CHANGELOG.md` entry under Removed, and verify `.pql/pql.db` still opens under the new `pql` binary.
|
||||
- **Cross-reference:** [D-40](../decisions/process.md#d-40-python-stopgap-under-toolsscriptsplan)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,13 @@
|
||||
# Rejected — Testing
|
||||
|
||||
Alternatives considered and rejected, with rationale preserved for
|
||||
future reference.
|
||||
|
||||
---
|
||||
|
||||
### R-5: Patrol test runner
|
||||
- **Rejected:** 2026-04-21
|
||||
- **Reason:** Adds a dependency (violates [D-31](../decisions/tooling.md#d-31-prefer-zero-deps-exact-pin)) for a capability we get from Playwright + Flutter's own semantics tree. Patrol's value proposition (native-gesture emulation) is less relevant on Linux desktop than on mobile.
|
||||
- **Cross-reference:** [D-26](../decisions/testing.md#d-26-web-driver-raw-playwright-plus-flutter-semantics)
|
||||
|
||||
---
|
||||
@@ -5,24 +5,13 @@ import 'dart:io';
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import 'package:clide/src/terminal/terminal.dart';
|
||||
|
||||
import 'session_naming.dart';
|
||||
import 'tmux_session.dart' as tmux;
|
||||
|
||||
/// Claude pane. Opinionated per D-041:
|
||||
///
|
||||
/// - [isPrimary]=true: the session name is stable per repo
|
||||
/// (`clide-claude-<hash>`) so reopening the app re-attaches to a
|
||||
/// running `claude` under tmux. No close button rendered —
|
||||
/// close-gestures (tab × on the header) minimise, not kill.
|
||||
/// - [isPrimary]=false: session name includes a `-N` suffix for
|
||||
/// this clide run. Closes normally; `pane.close` kills the tmux
|
||||
/// session.
|
||||
///
|
||||
/// Requires `tmux` on the daemon's PATH. If it isn't there, the pane
|
||||
/// falls back to spawning `claude` directly and loses persistence —
|
||||
/// an explicit state message lands in the header subtitle.
|
||||
class ClaudePane extends StatefulWidget {
|
||||
const ClaudePane({
|
||||
super.key,
|
||||
@@ -33,8 +22,6 @@ class ClaudePane extends StatefulWidget {
|
||||
|
||||
final bool isPrimary;
|
||||
final bool showChrome;
|
||||
|
||||
/// 1-based secondary-session index. Ignored when [isPrimary].
|
||||
final int? secondaryIndex;
|
||||
|
||||
@override
|
||||
@@ -42,7 +29,8 @@ class ClaudePane extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ClaudePaneState extends State<ClaudePane> {
|
||||
static const _maxLines = 5000;
|
||||
static const _maxLines = 50000;
|
||||
static String? _tmuxConfPath;
|
||||
|
||||
late final Terminal _terminal;
|
||||
StreamSubscription<DaemonEvent>? _eventSub;
|
||||
@@ -57,8 +45,8 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_terminal = Terminal(maxLines: _maxLines);
|
||||
_terminal.onOutput = _onOutput;
|
||||
_terminal.onResize = _onResize;
|
||||
_terminal.onOutput = _onTerminalOutput;
|
||||
_terminal.onResize = _onTerminalResize;
|
||||
// Don't spawn here — wait for the first onResize from TerminalView
|
||||
// so the PTY gets real dimensions, not 80x24 defaults.
|
||||
}
|
||||
@@ -70,24 +58,49 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
_eventSub?.cancel();
|
||||
_eventSub = null;
|
||||
final id = _paneId;
|
||||
final sessionName = _sessionName;
|
||||
_paneId = null;
|
||||
// Secondary panes own their tmux session — close on dispose.
|
||||
// Primary panes leave the tmux session alive so the next launch
|
||||
// re-attaches via `tmux new-session -A` (D-41).
|
||||
//
|
||||
// pane.close kills the PTY-spawned tmux *client*; the tmux server
|
||||
// keeps the session alive. We need an explicit kill-session for
|
||||
// secondaries to actually disappear (D-41 close semantics).
|
||||
if (id != null && !widget.isPrimary) {
|
||||
// Secondary: killing the pane kills the tmux session too —
|
||||
// that's the D-041 policy ("closing a secondary pops back to
|
||||
// primary"). The daemon's pane.close is idempotent.
|
||||
unawaited(_ipc()?.request('pane.close', args: {'id': id}));
|
||||
if (sessionName != null) {
|
||||
unawaited(tmux.killSession(sessionName));
|
||||
}
|
||||
}
|
||||
// Primary: don't close on dispose. The next time this pane is
|
||||
// rebuilt (next app launch, or tab reopen), tmux new-session -A
|
||||
// re-attaches to the same running claude.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// -- tmux config extraction -----------------------------------------------
|
||||
|
||||
static Future<String?> _ensureTmuxConf() async {
|
||||
if (_tmuxConfPath != null) return _tmuxConfPath;
|
||||
try {
|
||||
final content = await rootBundle.loadString('assets/clide.tmux.conf');
|
||||
final dir = Directory(
|
||||
'${Platform.environment['HOME'] ?? '/tmp'}/.config/clide',
|
||||
);
|
||||
if (!dir.existsSync()) dir.createSync(recursive: true);
|
||||
final file = File('${dir.path}/tmux.conf');
|
||||
file.writeAsStringSync(content);
|
||||
_tmuxConfPath = file.path;
|
||||
return _tmuxConfPath;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// -- spawn ----------------------------------------------------------------
|
||||
|
||||
Future<void> _spawnWhenReady() async {
|
||||
if (!mounted) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
if (!kernel.project.isOpen) {
|
||||
// Wait for a project to open before spawning.
|
||||
final c = Completer<void>();
|
||||
late final StreamSubscription<ProjectOpened> sub;
|
||||
sub = kernel.events.on<ProjectOpened>().listen((_) {
|
||||
@@ -106,13 +119,10 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
if (!mounted) return;
|
||||
final ipc = _ipc();
|
||||
if (ipc == null || !ipc.isConnected) {
|
||||
setState(() => _error = 'Daemon not connected. Start `clide --daemon`.');
|
||||
setState(() => _error = 'Daemon not connected.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve repo root via files.root. If that fails (no daemon, no
|
||||
// git root), fall back to cwd — the session name will just be
|
||||
// based on wherever the daemon is running.
|
||||
String repoRoot = Directory.current.path;
|
||||
final rootResp = await ipc.request('files.root');
|
||||
if (rootResp.ok) {
|
||||
@@ -121,13 +131,15 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
_sessionName = widget.isPrimary ? primarySessionName(repoRoot) : secondarySessionName(repoRoot, widget.secondaryIndex!);
|
||||
|
||||
// tmux-wrapped session for persistence (D-041).
|
||||
// -x/-y set the initial window size; without them tmux defaults
|
||||
// to a huge size when running inside a PTY without a real terminal.
|
||||
final tmuxConf = await _ensureTmuxConf();
|
||||
final cols = _terminal.viewWidth;
|
||||
final rows = _terminal.viewHeight;
|
||||
|
||||
var argv = <String>[
|
||||
'tmux',
|
||||
'-L',
|
||||
'clide',
|
||||
if (tmuxConf != null) ...['-f', tmuxConf],
|
||||
'new-session',
|
||||
'-A',
|
||||
'-s',
|
||||
@@ -136,33 +148,37 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
'$cols',
|
||||
'-y',
|
||||
'$rows',
|
||||
'claude',
|
||||
];
|
||||
print('[spawn] cols=${_terminal.viewWidth} rows=${_terminal.viewHeight}');
|
||||
|
||||
// CLAUDE_CODE_NO_FLICKER=1 enables claude's fullscreen TUI mode:
|
||||
// input box pinned to the bottom of the alt-screen, claude owns
|
||||
// its own scrollback. Removes the need for tmux scroll forwarding.
|
||||
final env = {'CLAUDE_CODE_NO_FLICKER': '1'};
|
||||
|
||||
var resp = await ipc.request('pane.spawn', args: {
|
||||
'argv': argv,
|
||||
'kind': PaneKind.claude.wire,
|
||||
'cwd': repoRoot,
|
||||
'cols': _terminal.viewWidth,
|
||||
'rows': _terminal.viewHeight,
|
||||
'cols': cols,
|
||||
'rows': rows,
|
||||
'title': _sessionName,
|
||||
'env': env,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
// tmux probably missing — try bare claude so the pane still
|
||||
// works, at the cost of persistence.
|
||||
argv = ['claude'];
|
||||
resp = await ipc.request('pane.spawn', args: {
|
||||
'argv': argv,
|
||||
'kind': PaneKind.claude.wire,
|
||||
'cwd': repoRoot,
|
||||
'cols': _terminal.viewWidth,
|
||||
'rows': _terminal.viewHeight,
|
||||
'cols': cols,
|
||||
'rows': rows,
|
||||
'title': _sessionName,
|
||||
'env': env,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
setState(() {
|
||||
_error = resp.error?.message ?? 'spawn failed';
|
||||
});
|
||||
setState(() => _error = resp.error?.message ?? 'spawn failed');
|
||||
return;
|
||||
}
|
||||
setState(() => _statusLine = 'no-tmux · fresh every launch');
|
||||
@@ -172,11 +188,12 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
if (!mounted) return;
|
||||
_paneId = resp.data['id'] as String?;
|
||||
// PID available in resp.data['pid'] if needed for debugging.
|
||||
_subscribe();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// -- output batching ------------------------------------------------------
|
||||
|
||||
final _outputBuf = StringBuffer();
|
||||
Timer? _flushTimer;
|
||||
|
||||
@@ -197,30 +214,19 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
final b64 = e.data['bytes_b64'];
|
||||
if (b64 is String) {
|
||||
_outputBuf.write(utf8.decode(base64Decode(b64), allowMalformed: true));
|
||||
// Batch all output from the current event loop turn into one
|
||||
// terminal.write() call. scheduleMicrotask runs after all
|
||||
// pending events but before the next frame, so split escape
|
||||
// sequences within the same event batch are reunited.
|
||||
if (_flushTimer == null) {
|
||||
_flushTimer = Timer(Duration.zero, _flushOutput);
|
||||
}
|
||||
_flushTimer ??= Timer(Duration.zero, _flushOutput);
|
||||
}
|
||||
case 'pane.exit':
|
||||
if (widget.isPrimary) {
|
||||
// Primary exiting is unusual — tmux sessions survive
|
||||
// normal disconnects. Surface it but don't auto-respawn;
|
||||
// the user decides.
|
||||
setState(() => _statusLine = 'session exited — restart clide to retry');
|
||||
} else {
|
||||
setState(() => _statusLine = 'session exited');
|
||||
}
|
||||
setState(() => _statusLine = widget.isPrimary ? 'session exited — restart clide to retry' : 'session exited');
|
||||
case 'pane.closed':
|
||||
_paneId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onOutput(String text) {
|
||||
// -- terminal callbacks ---------------------------------------------------
|
||||
|
||||
void _onTerminalOutput(String text) {
|
||||
final id = _paneId;
|
||||
if (id == null) return;
|
||||
_ipc()?.request('pane.write', args: {'id': id, 'text': text});
|
||||
@@ -228,29 +234,35 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
|
||||
Timer? _resizeTimer;
|
||||
|
||||
void _onResize(int cols, int rows, int _, int __) {
|
||||
print('[onResize] cols=$cols rows=$rows spawned=$_spawned paneId=$_paneId');
|
||||
void _onTerminalResize(int cols, int rows, int _, int __) {
|
||||
if (!_spawned) {
|
||||
// First resize — TerminalView has real dimensions now.
|
||||
_spawned = true;
|
||||
_spawnWhenReady();
|
||||
return;
|
||||
}
|
||||
// Debounce resize — rapid SIGWINCH during window drag corrupts
|
||||
// the terminal rendering. Wait for the resize to settle.
|
||||
_resizeTimer?.cancel();
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 150), () {
|
||||
final id = _paneId;
|
||||
if (id == null) return;
|
||||
_ipc()?.request('pane.resize', args: {'id': id, 'cols': cols, 'rows': rows});
|
||||
// tmux sizes windows by client, not PTY winsize. Explicitly
|
||||
// resize the tmux window to match the TerminalView dimensions.
|
||||
if (_sessionName != null) {
|
||||
Process.run('tmux', ['resize-window', '-t', _sessionName!, '-x', '$cols', '-y', '$rows']);
|
||||
Process.run('tmux', [
|
||||
'-L',
|
||||
'clide',
|
||||
'resize-window',
|
||||
'-t',
|
||||
_sessionName!,
|
||||
'-x',
|
||||
'$cols',
|
||||
'-y',
|
||||
'$rows',
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -- helpers --------------------------------------------------------------
|
||||
|
||||
DaemonClient? _ipc() => _kernel()?.ipc;
|
||||
|
||||
KernelServices? _kernel() {
|
||||
@@ -261,15 +273,18 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
}
|
||||
}
|
||||
|
||||
// -- build ----------------------------------------------------------------
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = widget.isPrimary ? 'claude — primary' : 'claude — secondary ${widget.secondaryIndex}';
|
||||
|
||||
final body = _error != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ClideText(_error!, muted: true),
|
||||
)
|
||||
: ClidePtyView(terminal: _terminal, label: title);
|
||||
: ClidePtyView(terminal: _terminal, label: title, autofocus: true);
|
||||
|
||||
if (!widget.showChrome) return body;
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'claude_pane.dart';
|
||||
|
||||
/// Hosts the primary Claude pane plus N user-spawned secondary
|
||||
/// sessions per D-41. Uses [MultitabPane] for the tab strip
|
||||
/// (drag-reorder, close ×, + button) and [IndexedStack]-mode
|
||||
/// keep-alive so switching tabs doesn't tear down the underlying
|
||||
/// PTY-backed terminal.
|
||||
class ClaudeSessionHost extends StatefulWidget {
|
||||
const ClaudeSessionHost({super.key});
|
||||
|
||||
@@ -12,175 +16,67 @@ class ClaudeSessionHost extends StatefulWidget {
|
||||
}
|
||||
|
||||
class ClaudeSessionHostState extends State<ClaudeSessionHost> {
|
||||
final List<_Session> _sessions = [];
|
||||
int _activeIndex = 0;
|
||||
static const _primaryId = 'primary';
|
||||
|
||||
late final MultitabController<_Session> _controller;
|
||||
int _nextSecondary = 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sessions.add(_Session(isPrimary: true, label: 'primary'));
|
||||
_controller = MultitabController<_Session>(
|
||||
initial: [
|
||||
MultitabEntry<_Session>(
|
||||
id: _primaryId,
|
||||
title: 'primary',
|
||||
payload: const _Session(isPrimary: true),
|
||||
// Primary persists across clide restarts and never gets a
|
||||
// close affordance (D-41).
|
||||
closeable: false,
|
||||
reorderable: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Public entry point used by the `claude.new-secondary` command.
|
||||
void addSecondary() {
|
||||
final index = _nextSecondary++;
|
||||
setState(() {
|
||||
_sessions.add(_Session(isPrimary: false, secondaryIndex: index, label: 'session $index'));
|
||||
_activeIndex = _sessions.length - 1;
|
||||
});
|
||||
}
|
||||
|
||||
void _close(int index) {
|
||||
if (index < 0 || index >= _sessions.length) return;
|
||||
if (_sessions[index].isPrimary) return;
|
||||
setState(() {
|
||||
_sessions.removeAt(index);
|
||||
if (_activeIndex >= _sessions.length) _activeIndex = _sessions.length - 1;
|
||||
if (_activeIndex < 0) _activeIndex = 0;
|
||||
});
|
||||
_controller.add(MultitabEntry<_Session>(
|
||||
id: 'secondary-$index',
|
||||
title: 'session $index',
|
||||
payload: _Session(isPrimary: false, secondaryIndex: index),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final showTabs = _sessions.length > 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (showTabs)
|
||||
_TabRow(
|
||||
sessions: _sessions,
|
||||
activeIndex: _activeIndex,
|
||||
tokens: tokens,
|
||||
onSelect: (i) => setState(() => _activeIndex = i),
|
||||
onClose: _close,
|
||||
onAdd: addSecondary,
|
||||
),
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: _activeIndex,
|
||||
children: [
|
||||
for (final s in _sessions)
|
||||
ClaudePane(
|
||||
key: s.key,
|
||||
isPrimary: s.isPrimary,
|
||||
secondaryIndex: s.secondaryIndex,
|
||||
showChrome: !showTabs,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
return MultitabPane<_Session>(
|
||||
controller: _controller,
|
||||
keepAlive: true,
|
||||
onAddRequested: addSecondary,
|
||||
bodyBuilder: (ctx, entry) {
|
||||
final s = entry.payload;
|
||||
return ClaudePane(
|
||||
isPrimary: s.isPrimary,
|
||||
secondaryIndex: s.secondaryIndex,
|
||||
// The MultitabPane already provides the tab strip header;
|
||||
// suppressing the ClaudePane's own chrome avoids a double row.
|
||||
showChrome: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Session {
|
||||
_Session({required this.isPrimary, this.secondaryIndex, required this.label}) : key = GlobalKey();
|
||||
const _Session({required this.isPrimary, this.secondaryIndex});
|
||||
final bool isPrimary;
|
||||
final int? secondaryIndex;
|
||||
final String label;
|
||||
final GlobalKey key;
|
||||
}
|
||||
|
||||
class _TabRow extends StatelessWidget {
|
||||
const _TabRow({
|
||||
required this.sessions,
|
||||
required this.activeIndex,
|
||||
required this.tokens,
|
||||
required this.onSelect,
|
||||
required this.onClose,
|
||||
required this.onAdd,
|
||||
});
|
||||
|
||||
final List<_Session> sessions;
|
||||
final int activeIndex;
|
||||
final SurfaceTokens tokens;
|
||||
final ValueChanged<int> onSelect;
|
||||
final ValueChanged<int> onClose;
|
||||
final VoidCallback onAdd;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onDoubleTap: onAdd,
|
||||
child: Container(
|
||||
height: 28,
|
||||
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: tokens.dividerColor))),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var i = 0; i < sessions.length; i++)
|
||||
_Tab(
|
||||
session: sessions[i],
|
||||
active: i == activeIndex,
|
||||
tokens: tokens,
|
||||
onTap: () => onSelect(i),
|
||||
onClose: sessions[i].isPrimary ? null : () => onClose(i)),
|
||||
const SizedBox(width: 4),
|
||||
_AddButton(tokens: tokens, onTap: onAdd),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Tab extends StatelessWidget {
|
||||
const _Tab({required this.session, required this.active, required this.tokens, required this.onTap, this.onClose});
|
||||
final _Session session;
|
||||
final bool active;
|
||||
final SurfaceTokens tokens;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClideTappable(
|
||||
onTap: onTap,
|
||||
builder: (context, hovered, _) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: hovered && !active ? tokens.tabInactive : null,
|
||||
border: Border(bottom: BorderSide(color: active ? tokens.tabActiveBorder : const Color(0x00000000), width: 2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClideText(
|
||||
session.label,
|
||||
fontSize: 12,
|
||||
color: active ? tokens.tabActiveForeground : tokens.tabInactiveForeground,
|
||||
fontFamily: clideMonoFamily,
|
||||
),
|
||||
if (onClose != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
GestureDetector(
|
||||
onTap: onClose,
|
||||
child: ClideIcon(PhosphorIcons.xMark, size: 10, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddButton extends StatelessWidget {
|
||||
const _AddButton({required this.tokens, required this.onTap});
|
||||
final SurfaceTokens tokens;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClideTappable(
|
||||
onTap: onTap,
|
||||
tooltip: 'New session',
|
||||
builder: (context, hovered, _) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: ClideText('+', fontSize: 14, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:clide/clide.dart';
|
||||
import 'package:clide/builtin/claude/src/claude_session_host.dart';
|
||||
import 'package:clide/builtin/claude/src/session_naming.dart';
|
||||
import 'package:clide/builtin/claude/src/tmux_session.dart' as tmux;
|
||||
import 'package:clide/extension/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -48,29 +50,63 @@ class ClaudeExtension extends ClideExtension {
|
||||
@override
|
||||
Future<void> activate(ClideExtensionContext ctx) async {
|
||||
_ctx = ctx;
|
||||
// Cold-start reap: kill any leftover secondary tmux sessions from
|
||||
// a previous run. D-41's "secondary numbering resets between
|
||||
// clide runs" only holds if the leftovers are gone before the new
|
||||
// run starts. Doing this in activate (rather than the previous
|
||||
// run's deactivate) guarantees cleanup even after an abrupt exit
|
||||
// — Flutter's deactivate hook only fires on explicit extension
|
||||
// teardown, not on app quit / kill -9 / OOM.
|
||||
final primary = await _primarySessionName();
|
||||
if (primary != null) await tmux.reapSecondaries(primary);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deactivate() async {
|
||||
await _killAllSessions([]);
|
||||
// Best-effort cleanup on explicit extension teardown. The cold-
|
||||
// start reap in activate is the actual safety net.
|
||||
final primary = await _primarySessionName();
|
||||
if (primary != null) await tmux.reapSecondaries(primary);
|
||||
}
|
||||
|
||||
/// Hard-reset command: kill every clide-claude tmux session for this
|
||||
/// repo, primary included. The user invokes this when they want to
|
||||
/// start over — typically after a tmux/Claude wedge.
|
||||
Future<IpcResponse> _killAllSessions(List<String> args) async {
|
||||
final ctx = _ctx;
|
||||
if (ctx == null) return IpcResponse.ok(id: '', data: const {});
|
||||
|
||||
// Close the UI panes first so they don't try to talk to a tmux
|
||||
// server that's about to lose their sessions.
|
||||
final resp = await ctx.ipc.request('pane.list');
|
||||
if (!resp.ok) return resp;
|
||||
final panes = resp.data['panes'];
|
||||
if (panes is List) {
|
||||
for (final p in panes) {
|
||||
if (p is Map && p['kind'] == 'claude') {
|
||||
final id = p['id'] as String?;
|
||||
if (id != null) {
|
||||
await ctx.ipc.request('pane.close', args: {'id': id});
|
||||
if (resp.ok) {
|
||||
final panes = resp.data['panes'];
|
||||
if (panes is List) {
|
||||
for (final p in panes) {
|
||||
if (p is Map && p['kind'] == 'claude') {
|
||||
final id = p['id'] as String?;
|
||||
if (id != null) {
|
||||
await ctx.ipc.request('pane.close', args: {'id': id});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then kill the server-side sessions, primary included.
|
||||
final primary = await _primarySessionName();
|
||||
if (primary != null) await tmux.killAllForRepo(primary);
|
||||
|
||||
return IpcResponse.ok(id: '', data: const {'status': 'killed'});
|
||||
}
|
||||
|
||||
Future<String?> _primarySessionName() async {
|
||||
final ctx = _ctx;
|
||||
if (ctx == null) return null;
|
||||
final resp = await ctx.ipc.request('files.root');
|
||||
if (!resp.ok) return null;
|
||||
final root = resp.data['path'] as String?;
|
||||
if (root == null) return null;
|
||||
return primarySessionName(root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/// tmux server interactions for Claude panes (D-41 lifecycle).
|
||||
///
|
||||
/// `pane.close` only kills the PTY-spawned tmux *client*; tmux is
|
||||
/// client/server, so the server-side session keeps running after the
|
||||
/// client disconnects. To honour D-41 ("closing a secondary kills that
|
||||
/// tmux session" + "secondary numbering resets between clide runs"),
|
||||
/// we need explicit `tmux kill-session` calls — that's what lives here.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
/// Override-able runner so tests don't shell out for real.
|
||||
typedef TmuxRunner = Future<ProcessResult> Function(List<String> args);
|
||||
|
||||
TmuxRunner tmuxRunner = _defaultRunner;
|
||||
|
||||
Future<ProcessResult> _defaultRunner(List<String> args) => Process.run('tmux', args);
|
||||
|
||||
const _socket = ['-L', 'clide'];
|
||||
|
||||
/// Kill the named tmux session on the clide socket. No-op if the
|
||||
/// session does not exist (kill-session exits non-zero — we ignore it).
|
||||
Future<void> killSession(String name) async {
|
||||
await tmuxRunner([..._socket, 'kill-session', '-t', name]);
|
||||
}
|
||||
|
||||
/// Return the names of all sessions currently alive on the clide
|
||||
/// socket. Empty list if the server is not running.
|
||||
Future<List<String>> listClideSessions() async {
|
||||
final r = await tmuxRunner([..._socket, 'list-sessions', '-F', '#{session_name}']);
|
||||
if (r.exitCode != 0) return const [];
|
||||
return (r.stdout as String).split('\n').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
/// Kill every secondary clide-claude session whose name begins with
|
||||
/// [primaryName] and ends with `-<digits>`. Leaves the primary itself
|
||||
/// alive (D-41).
|
||||
Future<void> reapSecondaries(String primaryName) async {
|
||||
final pattern = RegExp('^${RegExp.escape(primaryName)}-\\d+\$');
|
||||
for (final s in await listClideSessions()) {
|
||||
if (pattern.hasMatch(s)) {
|
||||
await killSession(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill every clide-claude session for [primaryName], including the
|
||||
/// primary itself. Used by the explicit `claude.kill-all-sessions`
|
||||
/// command when the user wants a hard reset.
|
||||
Future<void> killAllForRepo(String primaryName) async {
|
||||
for (final s in await listClideSessions()) {
|
||||
if (s == primaryName || s.startsWith('$primaryName-')) {
|
||||
await killSession(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,8 +123,9 @@ class _DecisionsViewState extends State<DecisionsView> {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
if (_loading) return const Center(child: ClideText('Loading decisions...', muted: true));
|
||||
if (_error != null) return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
||||
if (_decisions.isEmpty)
|
||||
if (_decisions.isEmpty) {
|
||||
return const Padding(padding: EdgeInsets.all(12), child: ClideText('No decisions found.\nRun `pql decisions sync` to index.', muted: true));
|
||||
}
|
||||
|
||||
final lf = _filter.toLowerCase();
|
||||
final hasFilter = lf.isNotEmpty;
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:clide/builtin/decisions/src/decision_detail_view.dart';
|
||||
import 'package:clide/builtin/decisions/src/decisions_view.dart';
|
||||
import 'package:clide/extension/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
|
||||
class DecisionsExtension extends ClideExtension {
|
||||
|
||||
@@ -87,7 +87,7 @@ class ProblemsController extends ChangeNotifier {
|
||||
if (broken > 0) {
|
||||
found.add(Problem(
|
||||
source: 'decisions',
|
||||
message: '$broken broken cross-reference(s) in decisions/',
|
||||
message: '$broken broken cross-reference(s) in governance/',
|
||||
hint: 'Run: pql decisions validate',
|
||||
));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:clide/clide.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:xterm/xterm.dart';
|
||||
import 'package:clide/src/terminal/terminal.dart';
|
||||
|
||||
/// General-purpose terminal pane. Spawns the user's `$SHELL` under the
|
||||
/// daemon's PTY (via `pane.spawn`), feeds the `pane.output` event
|
||||
@@ -62,7 +62,7 @@ class _TerminalPaneState extends State<TerminalPane> {
|
||||
if (!mounted) return;
|
||||
final ipc = _kernelIpc();
|
||||
if (ipc == null || !ipc.isConnected) {
|
||||
setState(() => _error = 'Daemon not connected. Start `clide --daemon`.');
|
||||
setState(() => _error = 'Backend not connected.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:clide/builtin/tickets/src/ticket_detail_view.dart';
|
||||
import 'package:clide/builtin/tickets/src/tickets_view.dart';
|
||||
import 'package:clide/extension/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
|
||||
class TicketsExtension extends ClideExtension {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class TicketDetail {
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:clide/builtin/tickets/src/ticket_colors.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/kernel/src/events/message_bus.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
||||
@@ -12,35 +12,109 @@ class WelcomeView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final kernel = ClideKernel.of(context);
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 850),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_Header(tokens: tokens),
|
||||
const SizedBox(height: 56),
|
||||
Row(
|
||||
return LayoutBuilder(
|
||||
builder: (context, c) {
|
||||
// Tips card sits below the START/RECENT row when there's room
|
||||
// for it; on shorter viewports the two centered columns win
|
||||
// and the tips drop out cleanly.
|
||||
final showTips = c.maxHeight > 640;
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 850),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _StartColumn(tokens: tokens, kernel: kernel)),
|
||||
const SizedBox(width: 56),
|
||||
Expanded(child: _RecentColumn(tokens: tokens, kernel: kernel)),
|
||||
_Header(tokens: tokens),
|
||||
const SizedBox(height: 56),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _StartColumn(tokens: tokens, kernel: kernel)),
|
||||
const SizedBox(width: 56),
|
||||
Expanded(child: _RecentColumn(tokens: tokens, kernel: kernel)),
|
||||
],
|
||||
),
|
||||
if (showTips) ...[
|
||||
const SizedBox(height: 48),
|
||||
_TipsCard(tokens: tokens),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 64,
|
||||
right: 64,
|
||||
bottom: 24,
|
||||
child: _StatusLine(tokens: tokens, kernel: kernel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TipsCard extends StatelessWidget {
|
||||
const _TipsCard({required this.tokens});
|
||||
final SurfaceTokens tokens;
|
||||
|
||||
static const _tips = <(String, String)>[
|
||||
('Quick open', '⌘P'),
|
||||
('Command palette', '⌘⇧P'),
|
||||
('Toggle sidebar', '⌘B'),
|
||||
('Toggle context', '⌘J'),
|
||||
('Switch theme', '⌘K ⌘T'),
|
||||
('New Claude session', '⌘⇧C'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Split tips into two rows of three so the card lays out as a
|
||||
// 3-column grid matching the START/RECENT proportions above.
|
||||
final firstRow = _tips.sublist(0, 3);
|
||||
final secondRow = _tips.sublist(3);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.panelBackground,
|
||||
border: Border.all(color: tokens.panelBorder),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText('TIPS', fontSize: 12, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
|
||||
const SizedBox(height: 14),
|
||||
_tipRow(firstRow),
|
||||
const SizedBox(height: 8),
|
||||
_tipRow(secondRow),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tipRow(List<(String, String)> tips) {
|
||||
return Row(
|
||||
children: [
|
||||
for (var i = 0; i < tips.length; i++) ...[
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: ClideText(tips[i].$1, fontSize: 13, color: tokens.globalTextMuted)),
|
||||
ClideText(tips[i].$2, fontSize: 12, color: tokens.globalForeground, fontFamily: clideMonoFamily),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 64,
|
||||
right: 64,
|
||||
bottom: 24,
|
||||
child: _StatusLine(tokens: tokens, kernel: kernel),
|
||||
),
|
||||
if (i < tips.length - 1) const SizedBox(width: 24),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -61,7 +135,7 @@ class _Header extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClideText('clide', fontSize: 52, fontWeight: FontWeight.w300, color: tokens.globalForeground),
|
||||
ClideText('Flutter desktop IDE for Claude Code', muted: true, fontSize: 16),
|
||||
ClideText('IDE for Claude Code CLI', muted: true, fontSize: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
/// clide — Dart core library.
|
||||
///
|
||||
/// Shared by `bin/clide.dart` (CLI + daemon) and by the Flutter app
|
||||
/// under `app/` (which depends on this package via `path: ../`).
|
||||
///
|
||||
/// See:
|
||||
/// * `decisions/architecture.md` `D-005` — layout + language rationale.
|
||||
/// * `decisions/architecture.md` `D-006` — CLI + event contract.
|
||||
/// * `governance/decisions/architecture.md` `D-005` — layout + language rationale.
|
||||
/// * `governance/decisions/architecture.md` `D-006` — CLI + event contract.
|
||||
library;
|
||||
|
||||
// Flutter-app-visible surface. Deliberately **does not** export the
|
||||
// `pty/` or `panes/registry.dart` modules — those import `dart:ffi`
|
||||
// and pull in the PTY machinery that only runs on desktop. The
|
||||
// daemon entrypoint (`bin/clide.dart`) imports them via deep paths.
|
||||
// and pull in the PTY machinery that only runs on desktop.
|
||||
//
|
||||
// `Pane` + `PaneKind` + the event-sink interfaces travel here because
|
||||
// they're pure data types that both the app and the daemon reference.
|
||||
// they're pure data types referenced throughout the app.
|
||||
|
||||
export 'src/daemon/dispatcher.dart';
|
||||
export 'src/editor/buffer.dart';
|
||||
@@ -28,15 +24,14 @@ export 'src/pql/client.dart' show PqlClient, PqlException;
|
||||
export 'src/ipc/envelope.dart';
|
||||
export 'src/ipc/paths.dart';
|
||||
export 'src/ipc/schema_v1.dart';
|
||||
export 'src/ipc/server.dart';
|
||||
export 'src/panes/event_sink.dart';
|
||||
export 'src/panes/pane.dart' show Pane, PaneKind;
|
||||
|
||||
/// Build-time-stamped version string.
|
||||
///
|
||||
/// The Makefile's `build` target passes `--define=clideVersion=…` when
|
||||
/// invoking `dart compile exe`, stamping `project.yaml`'s `version:`
|
||||
/// plus the git short SHA and dirty marker.
|
||||
/// The Makefile's `build` target passes `--define=clideVersion=…`,
|
||||
/// stamping `pubspec.yaml`'s `version:` plus the git short SHA and
|
||||
/// dirty marker.
|
||||
const clideVersion = String.fromEnvironment(
|
||||
'clideVersion',
|
||||
defaultValue: '2.0.0-dev',
|
||||
|
||||