|
|
|
@@ -75,6 +75,15 @@ var morningHTML string
|
|
|
|
|
//go:embed events.html
|
|
|
|
|
var eventsHTML string
|
|
|
|
|
|
|
|
|
|
//go:embed tools.html
|
|
|
|
|
var toolsHTML string
|
|
|
|
|
|
|
|
|
|
//go:embed routines.html
|
|
|
|
|
var routinesHTML string
|
|
|
|
|
|
|
|
|
|
//go:embed chat.html
|
|
|
|
|
var chatPageHTML string
|
|
|
|
|
|
|
|
|
|
// shellHTML — the shell partial every page is wrapped in: "shellTop", the
|
|
|
|
|
// "sidebar" it calls, and "shellBottom". It used to be two Go string constants
|
|
|
|
|
// with the sidebar assembled by a strings.Builder, which is the one piece of
|
|
|
|
@@ -672,92 +681,6 @@ var toolsTmpl = template.Must(template.New("tools").Funcs(func() template.FuncMa
|
|
|
|
|
return m
|
|
|
|
|
}()).Parse(shellHTML + toolsHTML))
|
|
|
|
|
|
|
|
|
|
const toolsHTML = `{{template "shellTop" "tools"}}
|
|
|
|
|
<h1>Tools</h1>
|
|
|
|
|
<p class=hint>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</p>
|
|
|
|
|
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
|
|
|
|
|
<section class=card>
|
|
|
|
|
<h2 class=card-title>proposed <span class=badge>{{len .Proposed}}</span></h2>
|
|
|
|
|
{{if .Proposed}}<p class=hint>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable. A row in an <code>mcp:</code> scope came from an MCP server and already knows what it calls — check the command, then enable.</p>
|
|
|
|
|
<div class=scroll><table><tr><th>name</th><th>scope</th><th>from utterance</th><th>enable as</th></tr>
|
|
|
|
|
{{range .Proposed}}<tr>
|
|
|
|
|
<td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td>{{.Utterance}}</td>
|
|
|
|
|
<td><form method=post action=/tools>
|
|
|
|
|
<input type=hidden name=name value="{{.Name}}">
|
|
|
|
|
<input type=hidden name=scope value="{{.Scope}}">
|
|
|
|
|
<input type=hidden name=action value=enable>
|
|
|
|
|
<input type=text name=cmd class=input-wide placeholder="systemctl restart" value="{{join .Cmd " "}}" required>
|
|
|
|
|
<label><input type=checkbox name=destructive {{if .Destructive}}checked{{end}}> destructive</label>
|
|
|
|
|
<button class=btn>enable</button></form>
|
|
|
|
|
<form method=post action=/tools class=inline-form>
|
|
|
|
|
<input type=hidden name=name value="{{.Name}}">
|
|
|
|
|
<input type=hidden name=action value=dismiss>
|
|
|
|
|
<button class="btn btn-muted">dismiss</button></form></td>
|
|
|
|
|
</tr>{{end}}</table></div>
|
|
|
|
|
{{else}}<div class=empty>
|
|
|
|
|
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-search"/></svg>
|
|
|
|
|
<div>no proposed tools</div>
|
|
|
|
|
<div class=hint>maven will propose tools here when she needs help running an action</div>
|
|
|
|
|
</div>{{end}}
|
|
|
|
|
</section>
|
|
|
|
|
<section class=card>
|
|
|
|
|
<h2 class=card-title>enabled <span class=badge>{{len .Enabled}}</span></h2>
|
|
|
|
|
{{if .Enabled}}<div class=scroll><table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
|
|
|
|
|
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td><code>{{join .Cmd " "}}</code></td>
|
|
|
|
|
<td>{{if .Destructive}}<span class=red>destructive</span>{{end}}</td>
|
|
|
|
|
<td><form method=post action=/tools class=inline-form>
|
|
|
|
|
<input type=hidden name=name value="{{.Name}}">
|
|
|
|
|
<input type=hidden name=scope value="{{.Scope}}">
|
|
|
|
|
<input type=hidden name=action value=disable>
|
|
|
|
|
<button class=btn>disable</button></form></td></tr>{{end}}</table></div>
|
|
|
|
|
{{else}}<div class=empty>
|
|
|
|
|
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
|
|
|
|
|
<div>no tools enabled</div>
|
|
|
|
|
<div class=hint>enable proposed tools above, or ask maven to configure one</div>
|
|
|
|
|
</div>{{end}}
|
|
|
|
|
</section>
|
|
|
|
|
<section class=card>
|
|
|
|
|
<h2 class=card-title>MCP servers <span class=badge>{{len .MCP}}</span></h2>
|
|
|
|
|
{{if .MCP}}<p class=hint>servers she connects OUT to. Their tools appear above as proposals — a configured server is a place she may look, not a capability she has. A <code>stdio</code> target is a process on this box; an <code>http</code> one on a loopback or LAN address is inside the network, so treat its tools accordingly.</p>
|
|
|
|
|
<div class=scroll><table><tr><th>name</th><th>transport</th><th>target</th><th>state</th><th>tools</th></tr>
|
|
|
|
|
{{range .MCP}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Transport}}</span></td><td><code>{{.Target}}</code></td>
|
|
|
|
|
<td>{{if .Connected}}connected{{if .Server}} — {{.Server}}{{end}}{{else}}<span class=red>down</span>{{if .Err}} — {{.Err}}{{end}}{{end}}</td>
|
|
|
|
|
<td>{{.Tools}}</td></tr>{{end}}</table></div>
|
|
|
|
|
{{else}}<div class=empty>
|
|
|
|
|
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-settings"/></svg>
|
|
|
|
|
<div>no MCP servers configured</div>
|
|
|
|
|
<div class=hint>add an <code>mcp.servers</code> block to mavend.json to let her use an external tool server</div>
|
|
|
|
|
</div>{{end}}
|
|
|
|
|
</section>
|
|
|
|
|
{{template "shellBottom"}}`
|
|
|
|
|
|
|
|
|
|
// routinesHTML — proposed routine review surface. One row per thing maven
|
|
|
|
|
// noticed, in her words, with at most two actions: accept or dismiss.
|
|
|
|
|
const routinesHTML = `{{template "shellTop" "routines"}}
|
|
|
|
|
<h1>Routines</h1>
|
|
|
|
|
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
|
|
|
|
|
<section class=card>
|
|
|
|
|
<h2 class=card-title>noticed <span class=badge>{{len .Proposed}}</span></h2>
|
|
|
|
|
{{if .Proposed}}<div class=scroll><table><tr><th>maven noticed</th><th>when</th><th></th><th></th></tr>
|
|
|
|
|
{{range .Proposed}}<tr>
|
|
|
|
|
<td>{{.Phrase}}</td><td class=muted>{{.Noticed}}</td>
|
|
|
|
|
<td><form method=post action=/routines class=inline-form>
|
|
|
|
|
<input type=hidden name=id value="{{.ID}}">
|
|
|
|
|
<input type=hidden name=action value=accept>
|
|
|
|
|
<button class=btn>accept</button></form></td>
|
|
|
|
|
<td><form method=post action=/routines class=inline-form>
|
|
|
|
|
<input type=hidden name=id value="{{.ID}}">
|
|
|
|
|
<input type=hidden name=action value=dismiss>
|
|
|
|
|
<button class="btn btn-muted">dismiss</button></form></td>
|
|
|
|
|
</tr>{{end}}</table></div>
|
|
|
|
|
{{else}}<div class=empty>
|
|
|
|
|
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-wave"/></svg>
|
|
|
|
|
<div>no proposed routines</div>
|
|
|
|
|
<div class=hint>maven will propose routines here when she detects a recurring pattern</div>
|
|
|
|
|
</div>{{end}}
|
|
|
|
|
</section>
|
|
|
|
|
{{template "shellBottom"}}`
|
|
|
|
|
|
|
|
|
|
var historyTmpl = template.Must(template.New("history").Funcs(shellFuncs()).Parse(shellHTML + historyHTML))
|
|
|
|
|
|
|
|
|
|
var notificationsTmpl = template.Must(template.New("notifications").Funcs(shellFuncs()).Parse(shellHTML + notificationsHTML))
|
|
|
|
@@ -770,6 +693,8 @@ var voiceTmpl = template.Must(template.New("voice").Funcs(shellFuncs()).Parse(sh
|
|
|
|
|
|
|
|
|
|
var tasksTmpl = template.Must(template.New("tasks").Funcs(shellFuncs()).Parse(shellHTML + tasksHTML))
|
|
|
|
|
|
|
|
|
|
// routinesTmpl — the proposed-routine review surface. One row per thing maven
|
|
|
|
|
// noticed, in her words, with at most two actions: accept or dismiss.
|
|
|
|
|
var routinesTmpl = template.Must(template.New("routines").Funcs(shellFuncs()).Parse(shellHTML + routinesHTML))
|
|
|
|
|
|
|
|
|
|
var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMap {
|
|
|
|
@@ -1095,9 +1020,10 @@ func fmtTaskDate(t *time.Time) string {
|
|
|
|
|
return t.Local().Format("02 Jan")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// routineRow is one line on the page: what maven noticed, in her words, and
|
|
|
|
|
// how long ago she noticed it.
|
|
|
|
|
type routineRow struct {
|
|
|
|
|
// routineView is one line on the page: what maven noticed, in her words, and
|
|
|
|
|
// how long ago she noticed it. A view model, not a database row — the template
|
|
|
|
|
// never formats an interval or a timestamp itself.
|
|
|
|
|
type routineView struct {
|
|
|
|
|
ID int64
|
|
|
|
|
Phrase string
|
|
|
|
|
Noticed string
|
|
|
|
@@ -1159,23 +1085,23 @@ func handleRoutines(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, se
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
if err := routinesTmpl.Execute(w, struct {
|
|
|
|
|
Msg string
|
|
|
|
|
Proposed []routineRow
|
|
|
|
|
}{msg, routineRows(proposed)}); err != nil {
|
|
|
|
|
Proposed []routineView
|
|
|
|
|
}{msg, toRoutineViews(proposed)}); err != nil {
|
|
|
|
|
log.Printf("routines render: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// routineRows turns the wire rows into display rows. The phrase comes from
|
|
|
|
|
// toRoutineViews turns the wire rows into view models. The phrase comes from
|
|
|
|
|
// pattern.PhraseRoutine so the page says the same thing maven's voice says.
|
|
|
|
|
func routineRows(rs []ipc.ProposedRoutine) []routineRow {
|
|
|
|
|
out := make([]routineRow, 0, len(rs))
|
|
|
|
|
func toRoutineViews(rs []ipc.ProposedRoutine) []routineView {
|
|
|
|
|
out := make([]routineView, 0, len(rs))
|
|
|
|
|
for _, r := range rs {
|
|
|
|
|
p := pattern.ProposedRoutine{Action: r.Action, Object: r.Object, IntervalDays: r.IntervalDays}
|
|
|
|
|
noticed := "just now"
|
|
|
|
|
if r.CreatedTs > 0 {
|
|
|
|
|
noticed = time.Since(time.UnixMilli(r.CreatedTs)).Round(time.Minute).String() + " ago"
|
|
|
|
|
}
|
|
|
|
|
out = append(out, routineRow{ID: r.ID, Phrase: pattern.PhraseRoutine(&p), Noticed: noticed})
|
|
|
|
|
out = append(out, routineView{ID: r.ID, Phrase: pattern.PhraseRoutine(&p), Noticed: noticed})
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
@@ -1574,31 +1500,6 @@ func handlePTT(w http.ResponseWriter, r *http.Request, voiceAddr string, session
|
|
|
|
|
// and the handler redirects back to /chat with the response.
|
|
|
|
|
var chatTmpl = template.Must(template.New("chat").Funcs(shellFuncs()).Parse(shellHTML + chatPageHTML))
|
|
|
|
|
|
|
|
|
|
const chatPageHTML = `{{template "shellTop" "chat"}}
|
|
|
|
|
<h1>Chat</h1>
|
|
|
|
|
<section class=card>
|
|
|
|
|
{{if .Error}}<div class="msg msg-err">{{.Error}}</div>{{end}}
|
|
|
|
|
<div class="scroll chat-scroll" id=chatHistory>
|
|
|
|
|
{{range .Messages}}
|
|
|
|
|
<div class="chat-msg {{.Role}}"><strong>{{if eq .Role "user"}}you{{else}}maven{{end}}:</strong> {{.Text}}</div>
|
|
|
|
|
{{else}}
|
|
|
|
|
<div class=empty>
|
|
|
|
|
<svg class=icon width="20" height="20"><use href="/ethos-icons.svg#i-message"/></svg>
|
|
|
|
|
<div>start a conversation</div>
|
|
|
|
|
</div>
|
|
|
|
|
{{end}}
|
|
|
|
|
</div>
|
|
|
|
|
<form method=post action=/api/chat class=chat-form>
|
|
|
|
|
<input type=text name=text class=input-wide placeholder="type a message..." required autofocus>
|
|
|
|
|
<button class=btn>send</button>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
<script>
|
|
|
|
|
var ch = document.getElementById('chatHistory');
|
|
|
|
|
if(ch) ch.scrollTop = ch.scrollHeight;
|
|
|
|
|
</script>
|
|
|
|
|
{{template "shellBottom"}}`
|
|
|
|
|
|
|
|
|
|
// handleChatPage renders the chat conversation page.
|
|
|
|
|
func handleChatPage(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
|
|
|
if core == nil {
|
|
|
|
|