Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions internal/tui/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,15 @@ func (m *app) header() string {
loadToken = "warn"
}
health := []string{headerGauge("mem", sample.MemUsed, sample.MemTotal, m.opts.NoColor), headerGauge("swap", sample.SwapUsed, sample.SwapTotal, m.opts.NoColor), "load " + view.TokenStyle(loadToken, m.opts.NoColor).Render(view.FormatLoad(sample.Load1, "%.1f")) + fmt.Sprintf("/%d", sample.Cores), "disk " + view.HumanBytes(sample.DiskFree)}
right := strings.Join(health, " ")
identity = strings.Join(identityParts, " · ")
if lipgloss.Width(identity)+lipgloss.Width(right)+2 <= m.width {
return identity + strings.Repeat(" ", m.width-lipgloss.Width(identity)-lipgloss.Width(right)) + right
// One line at every width: drop health readings from the least important
// (disk, then load, then swap) before the header would wrap. Machines
// shows all of them.
for n := len(health); n > 0; n-- {
right := strings.Join(health[:n], " ")
if lipgloss.Width(identity)+lipgloss.Width(right)+2 <= m.width {
return identity + strings.Repeat(" ", m.width-lipgloss.Width(identity)-lipgloss.Width(right)) + right
}
}
return styledPack(append(identityParts, health...), m.width, " ")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/testdata/machine-80-expanded-false.golden
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
driga test-host · ● live · daemon 05:00:00 mem ███░░░ 50%
swap ███░░░ 50% load 0.0/4 disk 12.0G
driga test-host · ● live · daemon · 05:00:00 mem ███░░░ 50%
0 Flow 1 Projects [2 Machines]
────────────────────────────────────────────────────────────────────────────────
METRIC / APP / PROCESS CPU/# MEASURE / HOST / ATTRIBUTION MEM
Expand Down Expand Up @@ -44,6 +43,7 @@ swap ███░░░ 50% load 0.0/4 disk 12.0G




h health · p reclaim processes · d reclaim disk · 0 Flow · M machine
enter detail · / filter · s sort · g group by project · o open terminal
esc back · ? help · q quit
4 changes: 2 additions & 2 deletions internal/tui/testdata/machine-80-expanded-true.golden
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
driga test-host · ● live · daemon 05:00:00 mem ███░░░ 50%
swap ███░░░ 50% load 0.0/4 disk 12.0G
driga test-host · ● live · daemon · 05:00:00 mem ███░░░ 50%
0 Flow 1 Projects [2 Machines]
────────────────────────────────────────────────────────────────────────────────
METRIC / APP / PROCESS CPU/# MEASURE / HOST / ATTRIBUTION MEM
Expand Down Expand Up @@ -44,6 +43,7 @@ swap ███░░░ 50% load 0.0/4 disk 12.0G




h health · p reclaim processes · d reclaim disk · 0 Flow · M machine
enter detail · / filter · s sort · g group by project · o open terminal
esc back · ? help · q quit
9 changes: 6 additions & 3 deletions internal/view/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ type ThemeToken struct{ Name, Light, Dark string }

// Palette roles come from the official Catppuccin palettes (MIT): Latte for
// light, Mocha for dark. Where an official Latte color falls below 4.5:1 for
// text on Latte's base, text roles use readable(), the same hue darkened just
// enough. Tinted backgrounds are tint() mixes of the state color into base.
// text on Latte's mantle, the panel surface and darker than base, text roles
// use readable(), the same hue darkened just enough, so they pass on page and
// panels alike. Tinted backgrounds are tint() mixes of the state color into base.
var (
lb, mb = catppuccin.Latte.Base(), catppuccin.Mocha.Base()
pair = func(l, d catppuccin.Color) [2]string { return [2]string{hex(l), hex(d)} }
text = func(l, d catppuccin.Color) [2]string { return [2]string{readable(l, lb), hex(d)} }
text = func(l, d catppuccin.Color) [2]string {
return [2]string{readable(l, catppuccin.Latte.Mantle()), hex(d)}
}
)

func tok(name string, v [2]string) ThemeToken { return ThemeToken{name, v[0], v[1]} }
Expand Down
5 changes: 3 additions & 2 deletions internal/view/theme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,15 @@ func TestThemeFollowsCatppuccinAndStaysReadable(t *testing.T) {
if tok.Dark != strings.ToUpper(want[1].Hex) {
t.Errorf("%s dark %s, official Mocha %s", tok.Name, tok.Dark, want[1].Hex)
}
if c := Contrast(rgb(tok.Light), catppuccin.Latte.Base().RGB); c < 4.5 {
// Light text sits on Latte's base and on its darker mantle panels.
if c := Contrast(rgb(tok.Light), catppuccin.Latte.Mantle().RGB); c < 4.5 {
t.Errorf("%s light %s contrast %.2f", tok.Name, tok.Light, c)
}
if c := Contrast(rgb(tok.Dark), catppuccin.Mocha.Base().RGB); c < 4.5 {
t.Errorf("%s dark %s contrast %.2f", tok.Name, tok.Dark, c)
}
// Passing official colors are used unchanged.
if Contrast(want[0].RGB, catppuccin.Latte.Base().RGB) >= 4.5 && tok.Light != strings.ToUpper(want[0].Hex) {
if Contrast(want[0].RGB, catppuccin.Latte.Mantle().RGB) >= 4.5 && tok.Light != strings.ToUpper(want[0].Hex) {
t.Errorf("%s light %s should be official %s", tok.Name, tok.Light, want[0].Hex)
}
}
Expand Down
12 changes: 12 additions & 0 deletions internal/web/pages.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,18 @@ func templateFuncs() template.FuncMap {
},
"duration": func(d time.Duration) string { return d.Round(time.Second).String() },
"machineName": func(m view.MachineRow) string { return view.MachineName(m.Machine) },
// Chat's context chips show machine names, never raw machine IDs.
"machineNames": func(rows []view.MachineRow, local string) string {
names := map[string]string{}
for _, m := range rows {
names[m.MachineID] = view.MachineName(m.Machine)
if m.MachineID == local {
names["local"] = names[m.MachineID]
}
}
b, _ := json.Marshal(names)
return string(b)
},
"link": pageLink,
"agentPath": func(id int64) string { return fmt.Sprint("/agents/", id) },
"projectPath": func(id int64) string { return fmt.Sprint("/projects/", id) },
Expand Down
11 changes: 11 additions & 0 deletions internal/web/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ button:hover {
letter-spacing:.06em;
text-transform:uppercase;
}
/* Row grids are sized in ch: a smaller header font would shrink its columns
and misalign them with the rows, so only the header's cells are smaller. */
.r.hdr {
font-size:inherit;
}
.r.hdr > * {
font-size:11.5px;
}
.m,.muted {
color:var(--mid);
}
Expand Down Expand Up @@ -805,3 +813,6 @@ meter::-moz-meter-bar {
.app-notice{position:fixed;left:50%;bottom:24px;z-index:50;margin:0;padding:10px 16px;border:1px solid var(--line);border-radius:8px;background:var(--raise);color:var(--fg);box-shadow:0 8px 30px rgb(0 0 0 / .25);transform:translate(-50%,8px);opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}
.app-notice.show{opacity:1;transform:translate(-50%,0)}
@media (prefers-reduced-motion:reduce){.app-notice{transition:none}}

/* Prompt answers are the one decision on the page: at least 24 px targets. */
.prompt-answer{min-height:32px;padding:4px 14px}
2 changes: 1 addition & 1 deletion internal/web/static/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
pinned.count.textContent=more?"+"+more+" more waiting":"";pinned.count.hidden=!more;
}
function scope(){const q=new URLSearchParams(location.search),ctx={page:location.pathname,machine:q.get("machine")||"local"};const row=document.querySelector('[data-row].sel');const href=row&&row.getAttribute("href");let session=selectedSession||Number(row&&(row.dataset.session))||Number(q.get("id"))||0;if(href&&/^\/agents\/\d+/.test(href))session=Number(href.match(/\d+/)[0]);if(/^\/agents\/\d+$/.test(location.pathname))session=Number(location.pathname.split("/").pop());if(session)ctx.session_id=session;const map=document.querySelector("[data-flow-board]");if(map?.dataset.selectedMachine)ctx.machine=map.dataset.selectedMachine;const project=q.get("project")||document.querySelector(".status")?.dataset.chatProject||row?.dataset.chatProject;if(project)ctx.project=project;for(const key of removed)delete ctx[key];return ctx;}
function contextChips(){const node=document.getElementById("chat-context");const entries=Object.entries(scope());node.hidden=!entries.length;node.replaceChildren(...(entries.length?[document.createTextNode("Context ")]:[]));for(const [key,value]of entries){const b=document.createElement("button");b.type="button";b.className="chat-chip";b.textContent=(key==="session_id"?"session":key)+": "+value+" ×";b.addEventListener("click",()=>{removed.add(key);contextChips();});node.append(b);}}
function contextChips(){const node=document.getElementById("chat-context");const entries=Object.entries(scope());node.hidden=!entries.length;node.replaceChildren(...(entries.length?[document.createTextNode("Context ")]:[]));let names={};try{names=JSON.parse(node.dataset.machineNames||"{}");}catch(_){}for(const [key,value]of entries){const b=document.createElement("button");b.type="button";b.className="chat-chip";b.textContent=(key==="session_id"?"session":key)+": "+(key==="machine"&&names[value]||value)+" ×";b.addEventListener("click",()=>{removed.add(key);contextChips();});node.append(b);}}
function selectSession(session){selectedSession=Number(session);removed.delete("session_id");document.dispatchEvent(new CustomEvent("driga:select-session",{detail:{id:session}}));contextChips();}
log.addEventListener("click",event=>{const chip=event.target.closest("[data-chat-session]");if(chip)selectSession(chip.dataset.chatSession);});
function content(node,text){const pattern=/\[\[session:([1-9][0-9]*)\]\]/g;let end=0;for(const match of text.matchAll(pattern)){node.append(document.createTextNode(text.slice(end,match.index)));const chip=document.createElement("button");chip.type="button";chip.className="chat-chip";chip.textContent="session #"+match[1];chip.addEventListener("click",()=>{selectedSession=Number(match[1]);removed.delete("session_id");document.dispatchEvent(new CustomEvent("driga:select-session",{detail:{id:match[1]}}));contextChips();});node.append(chip);end=match.index+match[0].length;}node.append(document.createTextNode(text.slice(end)));}
Expand Down
2 changes: 1 addition & 1 deletion internal/web/templates/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<div class="chat-bottom">
<section id="chat-approval" class="chat-approval" aria-label="Pending approval" hidden></section>
<form id="chat-form" class="chat-compose">
<div id="chat-context" class="chat-context" aria-label="Context"></div>
<div id="chat-context" class="chat-context" aria-label="Context" data-machine-names="{{machineNames .Snapshot.Machines.Rows .Snapshot.LocalMachineID}}"></div>
<label class="sr-only" for="chat-input">Message to Driga</label><textarea id="chat-input" rows="3" maxlength="131072" placeholder="Ask driga anything…"></textarea>
<div class="chat-row"><span id="chat-status" class="chat-note" role="status"></span><span class="chat-spacer"></span><button id="chat-send" class="chat-send" type="submit">Send</button></div>
<div class="chat-note">Enter send · Shift+Enter new line · ↑ edit last · Esc blur / close · c toggle</div>
Expand Down
13 changes: 7 additions & 6 deletions internal/web/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ <h3>Latest handoffs

{{define "inbox"}}
{{if or (eq .Scope "all") (eq .Scope .LocalMachineID)}}<section{{if .Prompts}} class="pane"{{end}} data-prompts aria-label="Permission prompts">
{{range .Prompts}}<article class="item" data-prompt="{{.ID}}"><h3>{{.Tool}} · {{.Session}}</h3><p>{{.Cwd}}</p><pre>{{.Detail}}</pre><details><summary>Exact input</summary><pre>{{.InputText}}</pre></details><button type="button" data-prompt-answer="allow" data-prompt-id="{{.ID}}">Allow</button> <button type="button" data-prompt-answer="deny" data-prompt-id="{{.ID}}">Deny</button><span role="status"></span></article>{{end}}
{{range .Prompts}}<article class="item" data-prompt="{{.ID}}"><h3>{{.Tool}} · {{.Session}}</h3><p>{{.Cwd}}</p><pre>{{.Detail}}</pre><details><summary>Exact input</summary><pre>{{.InputText}}</pre></details><button type="button" class="prompt-answer" data-prompt-answer="allow" data-prompt-id="{{.ID}}" aria-label="Allow {{.Tool}} for {{.Session}}">Allow</button> <button type="button" class="prompt-answer" data-prompt-answer="deny" data-prompt-id="{{.ID}}" aria-label="Deny {{.Tool}} for {{.Session}}">Deny</button><span role="status"></span></article>{{end}}
</section>{{end}}
<p>{{if .All}}history · <a href="{{link "/inbox" .Scope false}}">current inbox</a>{{else}}Current waits and up to 50 completions from 24h · <a href="{{link "/inbox" .Scope true}}">show all history</a>{{end}}{{if .InboxPrevious}} · <a href="{{.InboxPrevious}}">previous page</a>{{end}}{{if .InboxNext}} · <a href="{{.InboxNext}}">next page</a>{{end}}</p>
{{if .AttentionOnly}}
Expand Down Expand Up @@ -258,8 +258,8 @@ <h3>{{machineName .}}
</p>
<p>{{.Agents}} agents · {{.Inbox}} inbox · last seen {{duration .LastSeenAge}} ago
</p>
<p class="muted">{{.Addr}} · {{.DrigaVersion}}
</p>{{if .LastError}}
{{if or .Addr .DrigaVersion}}<p class="muted">{{.Addr}}{{if and .Addr .DrigaVersion}} · {{end}}{{.DrigaVersion}}
</p>{{end}}{{if .LastError}}
<p class="notice">{{.LastError}}
</p>{{end}}{{if .SkewWarning}}
<p class="notice">{{.SkewWarning}}
Expand All @@ -278,15 +278,16 @@ <h3>{{.MachineName}} · Machine</h3>
{{range .Processes}}{{template "machine-process" .}}{{end}}</details>{{else}}<p class="empty">No live session costs.</p>{{end}}
</section>
<section data-machine-rank="agent" data-machine-id="{{.MachineID}}"><h4>Agent processes without a session</h4>
{{range .AgentNoSession}}{{template "machine-app" .}}{{end}}
{{range .AgentNoSession}}{{template "machine-app" .}}{{else}}<p class="empty">Every agent process belongs to a session.</p>{{end}}
</section>
<section aria-label="MCP servers"><h4>MCP servers and helper processes</h4>
{{range .MCPServers.Commands}}<details><summary>{{mcpSummary .}}</summary><pre>{{mcpDetails .}}</pre></details>{{end}}
{{range .MCPServers.Commands}}<details><summary>{{mcpSummary .}}</summary><pre>{{mcpDetails .}}</pre></details>{{else}}<p class="empty">No MCP servers running.</p>{{end}}
{{range .MCPServers.Pileups}}<article class="item">
<strong>{{.Name}} pile-up · {{.Count}} processes · {{bytes .Memory}} ({{.MemoryMeasure}}){{with mcpTrend .MCPCommand}} · {{.}}{{end}}</strong>
<p>Host: {{.Host.Name}} · PID {{.Host.PID}} · age {{duration .Host.Age}} · {{.Host.Cwd}}</p>
<p class="muted">Host started {{stamp .Host.StartedAt}} · oldest {{stamp .OldestStart}} · newest {{stamp .NewestStart}}</p>
<pre>{{mcpDetails .MCPCommand}}</pre><p>{{.Advice}}</p>
</article>{{else}}<p class="empty">No MCP pile-ups.</p>{{end}}</section></section>
</article>{{else}}<p class="empty">No MCP pile-ups.</p>{{end}}</section>
{{if .LikelyAgent}}<section data-machine-rank="likely" data-machine-id="{{.MachineID}}"><h4>Likely agent work (shared with editor)</h4>{{range .LikelyAgent}}{{template "machine-app" .}}{{end}}</section>{{end}}
<div data-machine-rank="other" data-machine-id="{{.MachineID}}">{{$other := machineOtherApps .}}<details data-machine-app="other"><summary>Other apps · {{bytes $other.Memory}} ({{$other.MemoryMeasure}}) · {{printf "%.1f" $other.CPUPercent}}% CPU</summary>
<div data-machine-rank="memory" data-machine-id="{{.MachineID}}"><h4>Top 10 apps by memory</h4>{{range .TopMemory}}{{template "machine-app" .}}{{else}}<p class="empty">No process readings.</p>{{end}}</div>
Expand Down
Loading