feat: add context/prompt API and bootstrap file resolution to Mini App
- ContextBuilder: workDir field, SetWorkDir(), ResolveBootstrapPaths() with project-scoped search (workDir → planWorkDir → workspace) - Mini App: /api/context and /api/prompt endpoints, SSE context events - DataProvider: GetContextInfo() and GetSystemPrompt() interface methods - loop.go: SetWorkDir per session, GetContextInfo() delegation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b43e797274
commit
62058922df
6 changed files with 231 additions and 18 deletions
|
|
@ -396,6 +396,24 @@ func (p *agentLoopDataProvider) GetActiveSessions() []miniapp.SessionInfo {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *agentLoopDataProvider) GetContextInfo() miniapp.ContextInfo {
|
||||||
|
workDir, planWorkDir, workspace, bootstrap := p.loop.GetContextInfo()
|
||||||
|
files := make([]miniapp.BootstrapFileInfo, len(bootstrap))
|
||||||
|
for i, b := range bootstrap {
|
||||||
|
files[i] = miniapp.BootstrapFileInfo{Name: b.Name, Path: b.Path, Scope: b.Scope}
|
||||||
|
}
|
||||||
|
return miniapp.ContextInfo{
|
||||||
|
WorkDir: workDir,
|
||||||
|
PlanWorkDir: planWorkDir,
|
||||||
|
Workspace: workspace,
|
||||||
|
Bootstrap: files,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *agentLoopDataProvider) GetSystemPrompt() string {
|
||||||
|
return p.loop.GetSystemPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *agentLoopDataProvider) GetGitRepos() []miniapp.GitRepoSummary {
|
func (p *agentLoopDataProvider) GetGitRepos() []miniapp.GitRepoSummary {
|
||||||
if time.Since(p.gitReposCacheAt) < gitCacheTTL {
|
if time.Since(p.gitReposCacheAt) < gitCacheTTL {
|
||||||
return p.gitReposCache
|
return p.gitReposCache
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
type ContextBuilder struct {
|
type ContextBuilder struct {
|
||||||
workspace string
|
workspace string
|
||||||
|
workDir string // session-specific working directory (worktree or project subdir)
|
||||||
skillsLoader *skills.SkillsLoader
|
skillsLoader *skills.SkillsLoader
|
||||||
memory *MemoryStore
|
memory *MemoryStore
|
||||||
tools *tools.ToolRegistry // Direct reference to tool registry
|
tools *tools.ToolRegistry // Direct reference to tool registry
|
||||||
|
|
@ -49,6 +50,12 @@ func (cb *ContextBuilder) SetToolsRegistry(registry *tools.ToolRegistry) {
|
||||||
cb.tools = registry
|
cb.tools = registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWorkDir sets the session-specific working directory (e.g., worktree path
|
||||||
|
// or project subdirectory). Bootstrap files found here take priority over workspace.
|
||||||
|
func (cb *ContextBuilder) SetWorkDir(dir string) {
|
||||||
|
cb.workDir = dir
|
||||||
|
}
|
||||||
|
|
||||||
// SetPeerNote sets the peer session awareness note for the current call.
|
// SetPeerNote sets the peer session awareness note for the current call.
|
||||||
func (cb *ContextBuilder) SetPeerNote(note string) {
|
func (cb *ContextBuilder) SetPeerNote(note string) {
|
||||||
cb.peerNote = note
|
cb.peerNote = note
|
||||||
|
|
@ -190,25 +197,88 @@ The following skills extend your capabilities. To use a skill, read its SKILL.md
|
||||||
return strings.Join(parts, "\n\n---\n\n")
|
return strings.Join(parts, "\n\n---\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
// BootstrapFileInfo describes a resolved bootstrap file.
|
||||||
bootstrapFiles := []string{
|
type BootstrapFileInfo struct {
|
||||||
"AGENTS.md",
|
Name string `json:"name"`
|
||||||
"SOUL.md",
|
Path string `json:"path"` // empty = not found
|
||||||
"USER.md",
|
Scope string `json:"scope"` // "project" or "global"
|
||||||
"IDENTITY.md",
|
}
|
||||||
|
|
||||||
|
// bootstrapFileSpec defines the search scope for each bootstrap file.
|
||||||
|
type bootstrapFileSpec struct {
|
||||||
|
Name string
|
||||||
|
Scope string // "project" = workDir→planWorkDir→workspace, "global" = workspace only
|
||||||
|
}
|
||||||
|
|
||||||
|
var bootstrapSpecs = []bootstrapFileSpec{
|
||||||
|
{Name: "AGENTS.md", Scope: "project"},
|
||||||
|
{Name: "IDENTITY.md", Scope: "project"},
|
||||||
|
{Name: "SOUL.md", Scope: "global"},
|
||||||
|
{Name: "USER.md", Scope: "global"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// bootstrapProjectDirs returns de-duplicated search directories for project-scoped files.
|
||||||
|
func (cb *ContextBuilder) bootstrapProjectDirs() []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
var dirs []string
|
||||||
|
for _, d := range []string{cb.workDir, cb.memory.GetPlanWorkDir(), cb.workspace} {
|
||||||
|
if d != "" && !seen[d] {
|
||||||
|
seen[d] = true
|
||||||
|
dirs = append(dirs, d)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||||
|
projectDirs := cb.bootstrapProjectDirs()
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, filename := range bootstrapFiles {
|
for _, spec := range bootstrapSpecs {
|
||||||
filePath := filepath.Join(cb.workspace, filename)
|
var dirs []string
|
||||||
|
if spec.Scope == "global" {
|
||||||
|
dirs = []string{cb.workspace}
|
||||||
|
} else {
|
||||||
|
dirs = projectDirs
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
filePath := filepath.Join(dir, spec.Name)
|
||||||
if data, err := os.ReadFile(filePath); err == nil {
|
if data, err := os.ReadFile(filePath); err == nil {
|
||||||
fmt.Fprintf(&sb, "## %s\n\n%s\n\n", filename, data)
|
fmt.Fprintf(&sb, "## %s\n\n%s\n\n", spec.Name, data)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveBootstrapPaths returns path resolution info for each bootstrap file
|
||||||
|
// using the same search logic as LoadBootstrapFiles.
|
||||||
|
func (cb *ContextBuilder) ResolveBootstrapPaths() []BootstrapFileInfo {
|
||||||
|
projectDirs := cb.bootstrapProjectDirs()
|
||||||
|
|
||||||
|
result := make([]BootstrapFileInfo, 0, len(bootstrapSpecs))
|
||||||
|
for _, spec := range bootstrapSpecs {
|
||||||
|
info := BootstrapFileInfo{Name: spec.Name, Scope: spec.Scope}
|
||||||
|
var dirs []string
|
||||||
|
if spec.Scope == "global" {
|
||||||
|
dirs = []string{cb.workspace}
|
||||||
|
} else {
|
||||||
|
dirs = projectDirs
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
filePath := filepath.Join(dir, spec.Name)
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
info.Path = filePath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, info)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (cb *ContextBuilder) BuildMessages(
|
func (cb *ContextBuilder) BuildMessages(
|
||||||
history []providers.Message,
|
history []providers.Message,
|
||||||
summary string,
|
summary string,
|
||||||
|
|
|
||||||
|
|
@ -811,6 +811,9 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
// 1. Update tool contexts
|
// 1. Update tool contexts
|
||||||
al.updateToolContexts(agent, opts.Channel, opts.ChatID)
|
al.updateToolContexts(agent, opts.Channel, opts.ChatID)
|
||||||
|
|
||||||
|
// 1a. Set session-specific working directory for bootstrap file lookup
|
||||||
|
agent.ContextBuilder.SetWorkDir(agent.EffectiveWorkspace(opts.SessionKey))
|
||||||
|
|
||||||
// 1b. Inject peer session awareness into system prompt
|
// 1b. Inject peer session awareness into system prompt
|
||||||
projectPath := agent.ContextBuilder.GetPlanWorkDir()
|
projectPath := agent.ContextBuilder.GetPlanWorkDir()
|
||||||
if projectPath == "" {
|
if projectPath == "" {
|
||||||
|
|
@ -2456,6 +2459,19 @@ func (al *AgentLoop) GetSessionStats() *stats.Stats {
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContextInfo returns the bootstrap file resolution and directory context for the default agent.
|
||||||
|
func (al *AgentLoop) GetContextInfo() (workDir, planWorkDir, workspace string, bootstrap []BootstrapFileInfo) {
|
||||||
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
if agent == nil {
|
||||||
|
return "", "", "", nil
|
||||||
|
}
|
||||||
|
workspace = agent.Workspace
|
||||||
|
planWorkDir = agent.ContextBuilder.GetPlanWorkDir()
|
||||||
|
workDir = agent.ContextBuilder.workDir
|
||||||
|
bootstrap = agent.ContextBuilder.ResolveBootstrapPaths()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// GetSystemPrompt returns the system prompt last sent to the LLM.
|
// GetSystemPrompt returns the system prompt last sent to the LLM.
|
||||||
// Falls back to building from current state if no LLM call has occurred yet.
|
// Falls back to building from current state if no LLM call has occurred yet.
|
||||||
func (al *AgentLoop) GetSystemPrompt() string {
|
func (al *AgentLoop) GetSystemPrompt() string {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,21 @@ type GitChange struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BootstrapFileInfo describes a resolved bootstrap file for the context API.
|
||||||
|
type BootstrapFileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextInfo describes the agent's directory context and bootstrap file resolution.
|
||||||
|
type ContextInfo struct {
|
||||||
|
WorkDir string `json:"work_dir"`
|
||||||
|
PlanWorkDir string `json:"plan_work_dir"`
|
||||||
|
Workspace string `json:"workspace"`
|
||||||
|
Bootstrap []BootstrapFileInfo `json:"bootstrap"`
|
||||||
|
}
|
||||||
|
|
||||||
// DataProvider is the read-only interface to agent state for the Mini App API.
|
// DataProvider is the read-only interface to agent state for the Mini App API.
|
||||||
type DataProvider interface {
|
type DataProvider interface {
|
||||||
ListSkills() []skills.SkillInfo
|
ListSkills() []skills.SkillInfo
|
||||||
|
|
@ -106,6 +121,8 @@ type DataProvider interface {
|
||||||
GetActiveSessions() []SessionInfo
|
GetActiveSessions() []SessionInfo
|
||||||
GetGitRepos() []GitRepoSummary
|
GetGitRepos() []GitRepoSummary
|
||||||
GetGitRepoDetail(name string) GitInfo
|
GetGitRepoDetail(name string) GitInfo
|
||||||
|
GetContextInfo() ContextInfo
|
||||||
|
GetSystemPrompt() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandSender injects a command into the message bus on behalf of a user.
|
// CommandSender injects a command into the message bus on behalf of a user.
|
||||||
|
|
@ -516,6 +533,8 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/miniapp/api/session", h.requireAuth(h.apiSession))
|
mux.HandleFunc("/miniapp/api/session", h.requireAuth(h.apiSession))
|
||||||
mux.HandleFunc("/miniapp/api/sessions", h.requireAuth(h.apiSessions))
|
mux.HandleFunc("/miniapp/api/sessions", h.requireAuth(h.apiSessions))
|
||||||
mux.HandleFunc("/miniapp/api/command", h.requireAuth(h.apiCommand))
|
mux.HandleFunc("/miniapp/api/command", h.requireAuth(h.apiCommand))
|
||||||
|
mux.HandleFunc("/miniapp/api/context", h.requireAuth(h.apiContext))
|
||||||
|
mux.HandleFunc("/miniapp/api/prompt", h.requireAuth(h.apiPrompt))
|
||||||
mux.HandleFunc("/miniapp/api/git", h.requireAuth(h.apiGit))
|
mux.HandleFunc("/miniapp/api/git", h.requireAuth(h.apiGit))
|
||||||
mux.HandleFunc("/miniapp/api/dev", h.requireAuth(h.apiDev))
|
mux.HandleFunc("/miniapp/api/dev", h.requireAuth(h.apiDev))
|
||||||
mux.HandleFunc("/miniapp/api/events", h.requireAuth(h.apiEvents))
|
mux.HandleFunc("/miniapp/api/events", h.requireAuth(h.apiEvents))
|
||||||
|
|
@ -604,6 +623,14 @@ func (h *Handler) apiSession(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, s)
|
writeJSON(w, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) apiContext(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, h.provider.GetContextInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) apiPrompt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSON(w, map[string]string{"prompt": h.provider.GetSystemPrompt()})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) apiGit(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) apiGit(w http.ResponseWriter, r *http.Request) {
|
||||||
repo := r.URL.Query().Get("repo")
|
repo := r.URL.Query().Get("repo")
|
||||||
if repo == "" {
|
if repo == "" {
|
||||||
|
|
@ -759,7 +786,7 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
ch := h.notifier.Subscribe()
|
ch := h.notifier.Subscribe()
|
||||||
defer h.notifier.Unsubscribe(ch)
|
defer h.notifier.Unsubscribe(ch)
|
||||||
|
|
||||||
var lastPlan, lastSession, lastSkills, lastDev []byte
|
var lastPlan, lastSession, lastSkills, lastDev, lastContext []byte
|
||||||
|
|
||||||
// Send initial state immediately
|
// Send initial state immediately
|
||||||
sendSSEIfChanged(w, flusher, "plan", h.provider.GetPlanInfo(), &lastPlan)
|
sendSSEIfChanged(w, flusher, "plan", h.provider.GetPlanInfo(), &lastPlan)
|
||||||
|
|
@ -768,6 +795,7 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
&lastSession)
|
&lastSession)
|
||||||
sendSSEIfChanged(w, flusher, "skills", h.provider.ListSkills(), &lastSkills)
|
sendSSEIfChanged(w, flusher, "skills", h.provider.ListSkills(), &lastSkills)
|
||||||
sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev)
|
sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev)
|
||||||
|
sendSSEIfChanged(w, flusher, "context", h.provider.GetContextInfo(), &lastContext)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -782,6 +810,7 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
&lastSession)
|
&lastSession)
|
||||||
sendSSEIfChanged(w, flusher, "skills", h.provider.ListSkills(), &lastSkills)
|
sendSSEIfChanged(w, flusher, "skills", h.provider.ListSkills(), &lastSkills)
|
||||||
sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev)
|
sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev)
|
||||||
|
sendSSEIfChanged(w, flusher, "context", h.provider.GetContextInfo(), &lastContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,12 @@ func (m *mockDataProvider) GetGitRepos() []GitRepoSummary {
|
||||||
func (m *mockDataProvider) GetGitRepoDetail(name string) GitInfo {
|
func (m *mockDataProvider) GetGitRepoDetail(name string) GitInfo {
|
||||||
return GitInfo{Name: name}
|
return GitInfo{Name: name}
|
||||||
}
|
}
|
||||||
|
func (m *mockDataProvider) GetContextInfo() ContextInfo {
|
||||||
|
return ContextInfo{Workspace: "/mock/workspace"}
|
||||||
|
}
|
||||||
|
func (m *mockDataProvider) GetSystemPrompt() string {
|
||||||
|
return "mock system prompt"
|
||||||
|
}
|
||||||
|
|
||||||
type mockSender struct{}
|
type mockSender struct{}
|
||||||
|
|
||||||
|
|
@ -391,8 +397,8 @@ func TestSSE_NotifyDrivesSubsequentEvents(t *testing.T) {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
// Drain initial 4 events (plan, session, skills, dev)
|
// Drain initial 5 events (plan, session, skills, dev, context)
|
||||||
drainEvents(t, scanner, 4, 2*time.Second)
|
drainEvents(t, scanner, 5, 2*time.Second)
|
||||||
|
|
||||||
// Mutate state and notify — diff dedup should detect the change and send a new event
|
// Mutate state and notify — diff dedup should detect the change and send a new event
|
||||||
provider.mutated.Store(true)
|
provider.mutated.Store(true)
|
||||||
|
|
@ -420,8 +426,8 @@ func TestSSE_DiffDedupSuppressesDuplicate(t *testing.T) {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
// Drain initial events (plan, session, skills, dev)
|
// Drain initial events (plan, session, skills, dev, context)
|
||||||
drainEvents(t, scanner, 4, 2*time.Second)
|
drainEvents(t, scanner, 5, 2*time.Second)
|
||||||
|
|
||||||
// Notify with unchanged data — should produce zero new event lines
|
// Notify with unchanged data — should produce zero new event lines
|
||||||
notifier.Notify()
|
notifier.Notify()
|
||||||
|
|
@ -474,6 +480,12 @@ func (m *mutatingDataProvider) GetGitRepos() []GitRepoSummary {
|
||||||
func (m *mutatingDataProvider) GetGitRepoDetail(name string) GitInfo {
|
func (m *mutatingDataProvider) GetGitRepoDetail(name string) GitInfo {
|
||||||
return GitInfo{Name: name}
|
return GitInfo{Name: name}
|
||||||
}
|
}
|
||||||
|
func (m *mutatingDataProvider) GetContextInfo() ContextInfo {
|
||||||
|
return ContextInfo{Workspace: "/mock/workspace"}
|
||||||
|
}
|
||||||
|
func (m *mutatingDataProvider) GetSystemPrompt() string {
|
||||||
|
return "mock system prompt"
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dev proxy tests ──
|
// ── Dev proxy tests ──
|
||||||
|
|
||||||
|
|
@ -1678,8 +1690,8 @@ func TestSSE_DevEventUpdatesOnActivateDeactivate(t *testing.T) {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(resp.Body)
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
// Drain initial 4 events
|
// Drain initial 5 events
|
||||||
drainEvents(t, scanner, 4, 2*time.Second)
|
drainEvents(t, scanner, 5, 2*time.Second)
|
||||||
|
|
||||||
// Activate — should trigger a dev event with active=true
|
// Activate — should trigger a dev event with active=true
|
||||||
h.ActivateDevTarget(id)
|
h.ActivateDevTarget(id)
|
||||||
|
|
|
||||||
|
|
@ -797,6 +797,7 @@
|
||||||
<div id="session" class="panel">
|
<div id="session" class="panel">
|
||||||
<div class="loading" id="session-loading">Loading session...</div>
|
<div class="loading" id="session-loading">Loading session...</div>
|
||||||
<div id="session-content" class="hidden"></div>
|
<div id="session-content" class="hidden"></div>
|
||||||
|
<div id="context-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="config" class="panel">
|
<div id="config" class="panel">
|
||||||
|
|
@ -1310,15 +1311,79 @@ function renderSessionFromData(sessions, stats) {
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cachedContextInfo = null;
|
||||||
|
|
||||||
|
function renderContextCard(ctx) {
|
||||||
|
if (!ctx) return '';
|
||||||
|
cachedContextInfo = ctx;
|
||||||
|
var wd = ctx.work_dir || '\u2014';
|
||||||
|
var pwd = ctx.plan_work_dir || '\u2014';
|
||||||
|
var ws = ctx.workspace || '\u2014';
|
||||||
|
var filesHtml = '';
|
||||||
|
if (ctx.bootstrap && ctx.bootstrap.length) {
|
||||||
|
filesHtml = ctx.bootstrap.map(function(b) {
|
||||||
|
var path = b.path ? escapeHtml(b.path) : '\u2014';
|
||||||
|
var scope = b.scope === 'global' ? 'global' : 'project';
|
||||||
|
var found = b.path ? 'var(--text)' : 'var(--hint)';
|
||||||
|
return `<div style="display:flex;gap:8px;padding:2px 0;font-size:12px">
|
||||||
|
<span style="min-width:90px;font-weight:600;color:${found}">${escapeHtml(b.name)}</span>
|
||||||
|
<span style="color:var(--hint);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${path}">${path}</span>
|
||||||
|
<span style="color:var(--hint);font-size:11px">${scope}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
return `<div class="card glass">
|
||||||
|
<div class="card-title">Context</div>
|
||||||
|
<div style="font-size:12px">
|
||||||
|
<div class="stat-row"><span class="stat-label">workDir</span><span class="stat-value" style="font-size:12px;overflow:hidden;text-overflow:ellipsis" title="${escapeHtml(wd)}">${escapeHtml(wd)}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">planWorkDir</span><span class="stat-value" style="font-size:12px;overflow:hidden;text-overflow:ellipsis" title="${escapeHtml(pwd)}">${escapeHtml(pwd)}</span></div>
|
||||||
|
<div class="stat-row"><span class="stat-label">workspace</span><span class="stat-value" style="font-size:12px;overflow:hidden;text-overflow:ellipsis" title="${escapeHtml(ws)}">${escapeHtml(ws)}</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px">${filesHtml}</div>
|
||||||
|
<div style="margin-top:8px;text-align:center">
|
||||||
|
<button onclick="toggleSystemPrompt()" style="background:var(--secondary-bg);color:var(--text);border:none;padding:6px 12px;border-radius:8px;font-size:12px;cursor:pointer" id="prompt-toggle-btn">Show System Prompt</button>
|
||||||
|
</div>
|
||||||
|
<pre id="system-prompt-view" style="display:none;margin-top:8px;font-size:11px;max-height:400px;overflow:auto;background:var(--secondary-bg);padding:8px;border-radius:6px;white-space:pre-wrap;word-break:break-word"></pre>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSystemPrompt() {
|
||||||
|
var view = document.getElementById('system-prompt-view');
|
||||||
|
var btn = document.getElementById('prompt-toggle-btn');
|
||||||
|
if (!view || !btn) return;
|
||||||
|
if (view.style.display === 'none') {
|
||||||
|
btn.textContent = 'Loading...';
|
||||||
|
apiFetch('/miniapp/api/prompt').then(function(data) {
|
||||||
|
view.textContent = data.prompt || '(empty)';
|
||||||
|
view.style.display = 'block';
|
||||||
|
btn.textContent = 'Hide System Prompt';
|
||||||
|
}).catch(function() {
|
||||||
|
btn.textContent = 'Show System Prompt';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
view.style.display = 'none';
|
||||||
|
btn.textContent = 'Show System Prompt';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContextFromData(ctx) {
|
||||||
|
var el = document.getElementById('context-content');
|
||||||
|
if (el) el.innerHTML = renderContextCard(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
function loadSession() {
|
function loadSession() {
|
||||||
return loadTab('session-loading', 'session-content', 'session',
|
return loadTab('session-loading', 'session-content', 'session',
|
||||||
function() {
|
function() {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
apiFetch('/miniapp/api/session'),
|
apiFetch('/miniapp/api/session'),
|
||||||
apiFetch('/miniapp/api/sessions').catch(function() { return []; }),
|
apiFetch('/miniapp/api/sessions').catch(function() { return []; }),
|
||||||
|
apiFetch('/miniapp/api/context').catch(function() { return null; }),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
function(results) { renderSessionFromData(results[1], results[0]); });
|
function(results) {
|
||||||
|
renderSessionFromData(results[1], results[0]);
|
||||||
|
renderContextFromData(results[2]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTokens(n) {
|
function formatTokens(n) {
|
||||||
|
|
@ -1537,6 +1602,9 @@ function connectSSE() {
|
||||||
eventSource.addEventListener('dev', function(e) {
|
eventSource.addEventListener('dev', function(e) {
|
||||||
try { lastSSE.dev = Date.now(); renderDevFromData(JSON.parse(e.data)); } catch(err) {}
|
try { lastSSE.dev = Date.now(); renderDevFromData(JSON.parse(e.data)); } catch(err) {}
|
||||||
});
|
});
|
||||||
|
eventSource.addEventListener('context', function(e) {
|
||||||
|
try { renderContextFromData(JSON.parse(e.data)); } catch(err) {}
|
||||||
|
});
|
||||||
eventSource.onerror = function() {
|
eventSource.onerror = function() {
|
||||||
// Browser will auto-reconnect EventSource
|
// Browser will auto-reconnect EventSource
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue