opencode-extended-sidebar 0.2.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.oesignore +17 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/oes.json +11 -0
- package/package.json +100 -0
- package/scripts/install-git-hooks.mjs +29 -0
- package/src/pware.oc.core/constants/index.ts +18 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +13 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -0
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -0
- package/src/pware.oc.core/git/index.ts +7 -0
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -0
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -0
- package/src/pware.oc.core/index.ts +18 -0
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -0
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -0
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -0
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -0
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -0
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -0
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -0
- package/src/pware.oc.core/pware.oc.core.oes.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -0
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -0
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +175 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -0
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -0
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -0
- package/src/pware.oc.omo/constants/index.ts +15 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -0
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -0
- package/src/pware.oc.omo/index.ts +7 -0
- package/src/pware.oc.omo/resolver/index.ts +19 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +361 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -0
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -0
- package/src/pware.oc.opencode/constants/index.ts +10 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -0
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -0
- package/src/pware.oc.opencode/index.ts +8 -0
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -0
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -0
- package/src/pware.oc.opencode/resolver/index.ts +170 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +162 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +185 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +211 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +26 -0
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +236 -0
- package/src/pware.oc.perf/index.ts +14 -0
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -0
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -0
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +927 -0
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -0
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -0
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -0
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +462 -0
- package/src/pware.oc.runtime/index.ts +13 -0
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +79 -0
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +253 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +95 -0
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +129 -0
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -0
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +43 -0
- package/src/pware.oc.runtime/resolver/index.ts +224 -0
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -0
- package/src/pware.oc.ui/index.ts +12 -0
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +339 -0
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -0
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -0
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -0
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -0
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +526 -0
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1629 -0
- package/src/pware.oc.ui.tsx +98 -0
|
@@ -0,0 +1,1629 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { createEffect, createMemo, createSignal, For, on, Show, onCleanup, type JSX } from "solid-js"
|
|
3
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
4
|
+
import type { BoxRenderable } from "@opentui/core"
|
|
5
|
+
import type { TuiPluginApi, TuiTheme } from "@opencode-ai/plugin/tui"
|
|
6
|
+
import {
|
|
7
|
+
DraftFile,
|
|
8
|
+
emptyOmo,
|
|
9
|
+
listApprovals,
|
|
10
|
+
sessionForPlanFile,
|
|
11
|
+
} from "../pware.oc.omo/resolver/index.js"
|
|
12
|
+
import {
|
|
13
|
+
delegatesForSession,
|
|
14
|
+
groupDelegates,
|
|
15
|
+
type RuntimeSnapshot,
|
|
16
|
+
} from "../pware.oc.runtime/resolver/index.js"
|
|
17
|
+
import {
|
|
18
|
+
MY_WORK_ORDER,
|
|
19
|
+
approvalContinueHint,
|
|
20
|
+
dropDismissed,
|
|
21
|
+
groupMyWork,
|
|
22
|
+
myWorkLabel,
|
|
23
|
+
toApprovalItems,
|
|
24
|
+
toDraftDocItems,
|
|
25
|
+
toPlanItems,
|
|
26
|
+
toQuestionItems,
|
|
27
|
+
toSessionItems,
|
|
28
|
+
type MyWorkItem,
|
|
29
|
+
type MyWorkKind,
|
|
30
|
+
} from "../pware.oc.runtime/pware.oc.runtime.mywork.js"
|
|
31
|
+
import {
|
|
32
|
+
emptyDb,
|
|
33
|
+
emptyProjectFeed,
|
|
34
|
+
isRealSession,
|
|
35
|
+
mergeTools,
|
|
36
|
+
readProjectFeed,
|
|
37
|
+
type ProjectFeed,
|
|
38
|
+
} from "../pware.oc.opencode/resolver/index.js"
|
|
39
|
+
import { type DocView } from "../pware.oc.omo/resolver/pware.oc.omo.resolver.doc.js"
|
|
40
|
+
import type { DelegateView } from "../pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.js"
|
|
41
|
+
import { enrichApprovalSessionStates } from "../pware.oc.runtime/pware.oc.runtime.mywork-enrich.js"
|
|
42
|
+
import {
|
|
43
|
+
ROW_LINE_FALLBACK,
|
|
44
|
+
ROW_LINE_RESERVE,
|
|
45
|
+
ROW_MIN,
|
|
46
|
+
ROW_RANK,
|
|
47
|
+
clampScrollOffset,
|
|
48
|
+
RT_ACTION_COL_WIDTH,
|
|
49
|
+
packSections,
|
|
50
|
+
panelRows,
|
|
51
|
+
rowsForPlan,
|
|
52
|
+
scrollByStep,
|
|
53
|
+
} from "../pware.oc.core/pware.oc.core.layout.js"
|
|
54
|
+
import { openReadonlyDb } from "../pware.oc.core/pware.oc.core.sqlite.js"
|
|
55
|
+
import {
|
|
56
|
+
MARK_QUEUED,
|
|
57
|
+
PULSE_LIVE,
|
|
58
|
+
PULSE_STALE,
|
|
59
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.pulse.js"
|
|
60
|
+
import {
|
|
61
|
+
STATUS_ARCHIVED,
|
|
62
|
+
TOOL_STATUS_RUNNING,
|
|
63
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.status.js"
|
|
64
|
+
import {
|
|
65
|
+
SESSION_STATUS_ARCHIVED,
|
|
66
|
+
} from "../pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.js"
|
|
67
|
+
import {
|
|
68
|
+
ROW_KIND_AGENT,
|
|
69
|
+
ROW_KIND_DELEGATE,
|
|
70
|
+
ROW_KIND_FILE,
|
|
71
|
+
ROW_KIND_GROUP,
|
|
72
|
+
ROW_KIND_TOOL,
|
|
73
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.rowKind.js"
|
|
74
|
+
import {
|
|
75
|
+
QUESTION_KIND_ERROR,
|
|
76
|
+
QUESTION_KIND_INTERRUPTED,
|
|
77
|
+
QUESTION_KIND_QUESTION,
|
|
78
|
+
} from "../pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.js"
|
|
79
|
+
import {
|
|
80
|
+
MY_WORK_GROUP_DISMISSED,
|
|
81
|
+
MY_WORK_GROUP_DRAFT_DOCS,
|
|
82
|
+
MY_WORK_GROUP_DRAFTING,
|
|
83
|
+
MY_WORK_GROUP_FINISHED,
|
|
84
|
+
MY_WORK_GROUP_PINNED,
|
|
85
|
+
MY_WORK_GROUP_PLANS,
|
|
86
|
+
MY_WORK_GROUP_READY_REVIEW,
|
|
87
|
+
MY_WORK_GROUP_READY_START,
|
|
88
|
+
MY_WORK_GROUP_SESSIONS,
|
|
89
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.myWork.js"
|
|
90
|
+
import { DOC_KIND_DRAFT, DOC_KIND_PLAN } from "../pware.oc.omo/constants/pware.oc.omo.constants.docKind.js"
|
|
91
|
+
import {
|
|
92
|
+
GROUP_GLYPH,
|
|
93
|
+
TAB_NEUTRAL_GLYPH,
|
|
94
|
+
tabAttentionGlyph,
|
|
95
|
+
engageDone,
|
|
96
|
+
engageFill,
|
|
97
|
+
fileLetterGlyph,
|
|
98
|
+
markTone,
|
|
99
|
+
myWorkGlyph,
|
|
100
|
+
reviewStateSuffix,
|
|
101
|
+
spinnerFrame,
|
|
102
|
+
type GlyphSpec,
|
|
103
|
+
type TabAttentionItem,
|
|
104
|
+
} from "./pware.oc.ui.glyphs.js"
|
|
105
|
+
import { dismissQuestion, kvRead, kvWrite, kvReadOne, kvWriteOne, readDismissedQuestions, ClickText, ContextActions, type ThemeColors } from "./pware.oc.ui.chrome.js"
|
|
106
|
+
import {
|
|
107
|
+
AgentLine,
|
|
108
|
+
FoldSection,
|
|
109
|
+
GroupSection,
|
|
110
|
+
RowList,
|
|
111
|
+
TabColumn,
|
|
112
|
+
OesStatusRow,
|
|
113
|
+
agentDisplayName,
|
|
114
|
+
useFold,
|
|
115
|
+
useReveal,
|
|
116
|
+
type RevealState,
|
|
117
|
+
type RowData,
|
|
118
|
+
} from "./pware.oc.ui.sections.js"
|
|
119
|
+
import { clipWidth } from "../pware.oc.core/pware.oc.core.width.js"
|
|
120
|
+
import { emptyPerf, readPerfSnapshot } from "../pware.oc.perf/pware.oc.perf.reader.js"
|
|
121
|
+
import { PerfPanel } from "../pware.oc.perf/pware.oc.perf.view.js"
|
|
122
|
+
import { shareBar } from "../pware.oc.perf/pware.oc.perf.charts.js"
|
|
123
|
+
import { StatRealtimeTimeline } from "../pware.oc.perf/pware.oc.perf.realtimeTimeline.js"
|
|
124
|
+
import { EventDriverSampler } from "../pware.oc.perf/pware.oc.perf.realtimeSampler.js"
|
|
125
|
+
import { readCpuRam } from "../pware.oc.perf/pware.oc.perf.realtimeCpuRam.js"
|
|
126
|
+
import { formatSelfLine, readRendererFps, readSelfStats, resetSelfStats, selfDiagActive, selfTime, setSelfFps } from "../pware.oc.perf/pware.oc.perf.self.js"
|
|
127
|
+
import {
|
|
128
|
+
decorateFiles,
|
|
129
|
+
fileFilter,
|
|
130
|
+
filesFromEvent,
|
|
131
|
+
mergeFiles,
|
|
132
|
+
sumDiff,
|
|
133
|
+
type FileView,
|
|
134
|
+
} from "../pware.oc.opencode/pware.oc.opencode.files.js"
|
|
135
|
+
import { onGitMarksChange } from "../pware.oc.core/git/pware.oc.core.git.js"
|
|
136
|
+
import { getOes } from "../pware.oc.core/pware.oc.core.oes.js"
|
|
137
|
+
import {
|
|
138
|
+
TAB_STATUS_SESSION_NOT_IN_DB,
|
|
139
|
+
tabStatus,
|
|
140
|
+
} from "../pware.oc.core/pware.oc.core.status.js"
|
|
141
|
+
import { createEventBus } from "../pware.oc.core/pware.oc.core.bus.js"
|
|
142
|
+
import { startRuntimeSource } from "../pware.oc.runtime/pware.oc.runtime.source.js"
|
|
143
|
+
import { openApprovalDialog, openDocDetail, openFileDetail, openFileListDialog, openQuestionDialog, openRealtimeCharts, openToolDetail } from "./pware.oc.ui.menudialogs.js"
|
|
144
|
+
import { startHostEventBridge } from "./pware.oc.ui.live.js"
|
|
145
|
+
import {
|
|
146
|
+
approvePlan,
|
|
147
|
+
openNewSessionPrompt,
|
|
148
|
+
openSessionSwitcher,
|
|
149
|
+
runStartWork,
|
|
150
|
+
selectSession,
|
|
151
|
+
} from "./pware.oc.ui.host.js"
|
|
152
|
+
import { dbg, debugActive, debugActiveDir, profile, profileActive, profileActiveDir, profileAsync, readScreenLines, subscribeScreenLines, writeProfileSummary } from "../pware.oc.core/pware.oc.core.debug.js"
|
|
153
|
+
import { getOpenCodeDbPath, samePath } from "../pware.oc.core/pware.oc.core.paths.js"
|
|
154
|
+
import {
|
|
155
|
+
FPS_READ_EVERY_TICKS,
|
|
156
|
+
GLYPH_TICK_MS,
|
|
157
|
+
MYWORK_BADGE_COOLDOWN_MS,
|
|
158
|
+
MYWORK_BADGE_MS,
|
|
159
|
+
NOW_MS,
|
|
160
|
+
SWITCH_TIMEOUT_MS,
|
|
161
|
+
TICK_MS,
|
|
162
|
+
TOKEN_RATE_WINDOW_MS,
|
|
163
|
+
} from "../pware.oc.core/pware.oc.core.timing.js"
|
|
164
|
+
import {
|
|
165
|
+
activeFlow,
|
|
166
|
+
applyFlow,
|
|
167
|
+
composeMark,
|
|
168
|
+
formatAge,
|
|
169
|
+
formatDuration,
|
|
170
|
+
hottestMark,
|
|
171
|
+
phaseAgeMs,
|
|
172
|
+
pulseAgeMs,
|
|
173
|
+
pushTokenTick,
|
|
174
|
+
tokenRate,
|
|
175
|
+
tokenRateBars,
|
|
176
|
+
toolFlow,
|
|
177
|
+
toolMark,
|
|
178
|
+
type AgentMark,
|
|
179
|
+
type FlowDir,
|
|
180
|
+
type FlowEntry,
|
|
181
|
+
type TokenTick,
|
|
182
|
+
type ToolHit,
|
|
183
|
+
} from "../pware.oc.core/pware.oc.core.pulse.js"
|
|
184
|
+
import {
|
|
185
|
+
EV_OES_SESSION_SELECT,
|
|
186
|
+
EV_OES_SNAPSHOT,
|
|
187
|
+
} from "../pware.oc.core/constants/pware.oc.core.constants.eventName.js"
|
|
188
|
+
import {
|
|
189
|
+
EV_OC_FILES_TOUCHED,
|
|
190
|
+
EV_OC_FLOW,
|
|
191
|
+
EV_OC_SESSION_ACTIVITY,
|
|
192
|
+
EV_OC_TOKENS_DELTA,
|
|
193
|
+
EV_OC_TOOL_HIT,
|
|
194
|
+
} from "../pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.js"
|
|
195
|
+
|
|
196
|
+
export type SidebarProps = {
|
|
197
|
+
sessionId: string
|
|
198
|
+
api: TuiPluginApi
|
|
199
|
+
theme: TuiTheme
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const KV_FOLD_AGENTS = "oes.fold.agents"
|
|
203
|
+
const KV_FOLD_DELEGATES = "oes.fold.delegates"
|
|
204
|
+
const KV_FOLD_TOOLS = "oes.fold.tools"
|
|
205
|
+
const KV_FOLD_FILES = "oes.fold.files"
|
|
206
|
+
const KV_FOLD_DRAFTS = "oes.fold.drafts"
|
|
207
|
+
const KV_TAB = "oes.tab"
|
|
208
|
+
|
|
209
|
+
const OES_TABS = ["mywork", "current", "sessions", "perf"] as const
|
|
210
|
+
type OesTab = (typeof OES_TABS)[number]
|
|
211
|
+
|
|
212
|
+
const TAB_LABELS: Record<string, string> = {
|
|
213
|
+
mywork: "My work",
|
|
214
|
+
sessions: "Project",
|
|
215
|
+
current: "Session",
|
|
216
|
+
perf: "Stats",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Event rows the bottom debug console shows at once (the ring keeps 200). */
|
|
220
|
+
const SCREEN_CONSOLE_VISIBLE = 5
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The cold-start "engage" bar (and its success toast) plays once per plugin
|
|
224
|
+
* boot. The module latch survives sidebar remounts across route changes so a
|
|
225
|
+
* session switch never replays the animation.
|
|
226
|
+
*/
|
|
227
|
+
let engageSeen = false
|
|
228
|
+
|
|
229
|
+
function emptyRuntime(): RuntimeSnapshot {
|
|
230
|
+
const dbPath = getOpenCodeDbPath()
|
|
231
|
+
return {
|
|
232
|
+
generatedAt: 0,
|
|
233
|
+
fingerprint: "",
|
|
234
|
+
scanStamp: "0",
|
|
235
|
+
db: emptyDb(dbPath),
|
|
236
|
+
omo: emptyOmo(),
|
|
237
|
+
omoConfig: { present: false, path: null, teamMode: null, agents: [] },
|
|
238
|
+
delegates: [],
|
|
239
|
+
openQuestions: [],
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function SidebarPanel(props: SidebarProps): JSX.Element {
|
|
244
|
+
const [snap, setSnap] = createSignal<RuntimeSnapshot>(emptyRuntime())
|
|
245
|
+
const [now, setNow] = createSignal(Date.now())
|
|
246
|
+
const [frame, setFrame] = createSignal(0)
|
|
247
|
+
const [glyphFrame, setGlyphFrame] = createSignal(0)
|
|
248
|
+
const [seen, setSeen] = createSignal<Record<string, number>>({})
|
|
249
|
+
const [busy, setBusy] = createSignal<Record<string, boolean>>({})
|
|
250
|
+
const [flow, setFlow] = createSignal<Record<string, FlowEntry>>({})
|
|
251
|
+
const [tab, setTab] = createSignal<OesTab>(kvReadOne(props.api, KV_TAB, "current", OES_TABS))
|
|
252
|
+
const dimensions = useTerminalDimensions()
|
|
253
|
+
/**
|
|
254
|
+
* Row width in columns, measured from the root box's computed layout width.
|
|
255
|
+
* `ref` fires before the first layout (width 0), so the oes-independent
|
|
256
|
+
* fallback holds until `onSizeChange` reports a real width. A scrollbar
|
|
257
|
+
* reserve is subtracted; the value is the same budget `lineMax` used to be.
|
|
258
|
+
*/
|
|
259
|
+
const [lineWidth, setLineWidth] = createSignal(ROW_LINE_FALLBACK)
|
|
260
|
+
let rootBox: BoxRenderable | undefined
|
|
261
|
+
const rootRef = (node: BoxRenderable): void => {
|
|
262
|
+
rootBox = node
|
|
263
|
+
node.onSizeChange = () => {
|
|
264
|
+
const w = node.width
|
|
265
|
+
if (Number.isFinite(w) && w > 0) setLineWidth(Math.max(ROW_LINE_FALLBACK, w - ROW_LINE_RESERVE))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const lineMax = (): number => lineWidth()
|
|
269
|
+
const [liveTools, setLiveTools] = createSignal<Record<string, ToolHit>>({})
|
|
270
|
+
const [liveFiles, setLiveFiles] = createSignal<Record<string, FileView>>({})
|
|
271
|
+
const [tokenTicks, setTokenTicks] = createSignal<readonly TokenTick[]>([])
|
|
272
|
+
const [rtVersion, setRtVersion] = createSignal(0)
|
|
273
|
+
const [gitTick, setGitTick] = createSignal(0)
|
|
274
|
+
const [switching, setSwitching] = createSignal<{ id: string; at: number } | null>(null)
|
|
275
|
+
const [coldTab, setColdTab] = createSignal<string | null>(null)
|
|
276
|
+
const [consoleLines, setConsoleLines] = createSignal(readScreenLines())
|
|
277
|
+
const [consoleTop, setConsoleTop] = createSignal(0)
|
|
278
|
+
const consoleWindow = createMemo(() => {
|
|
279
|
+
const lines = consoleLines()
|
|
280
|
+
const top = clampScrollOffset(lines.length, SCREEN_CONSOLE_VISIBLE, consoleTop())
|
|
281
|
+
return lines.slice(top, top + SCREEN_CONSOLE_VISIBLE)
|
|
282
|
+
})
|
|
283
|
+
/** Console rows the sidebar must leave for the debug block: label + visible lines. */
|
|
284
|
+
const consoleRowSpan = () =>
|
|
285
|
+
selfDiagActive() ? 1 + Math.min(consoleLines().length, SCREEN_CONSOLE_VISIBLE) : 0
|
|
286
|
+
|
|
287
|
+
const engageBootFrame = glyphFrame()
|
|
288
|
+
const [engaging, setEngaging] = createSignal(!engageSeen)
|
|
289
|
+
const engageTick = (): number => Math.max(0, glyphFrame() - engageBootFrame)
|
|
290
|
+
|
|
291
|
+
const colors = (): ThemeColors => props.theme.current as unknown as ThemeColors
|
|
292
|
+
|
|
293
|
+
const requestRender = () => {
|
|
294
|
+
try {
|
|
295
|
+
profile("requestRender", () => props.api.renderer.requestRender())
|
|
296
|
+
} catch {
|
|
297
|
+
// teardown
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const offConsole = subscribeScreenLines(() => {
|
|
302
|
+
setConsoleLines(readScreenLines())
|
|
303
|
+
if (consoleTop() > 0) setConsoleTop((top) => top + 1)
|
|
304
|
+
requestRender()
|
|
305
|
+
})
|
|
306
|
+
onCleanup(offConsole)
|
|
307
|
+
|
|
308
|
+
const onConsoleScroll = (e: { scroll?: { direction?: "up" | "down" | "left" | "right"; delta?: number } }) => {
|
|
309
|
+
const dir = e.scroll?.direction
|
|
310
|
+
if (dir !== "up" && dir !== "down") return
|
|
311
|
+
const step = Math.max(1, Math.round(Math.abs(e.scroll?.delta ?? 1)) || 1)
|
|
312
|
+
setConsoleTop((top) => scrollByStep(consoleLines().length, SCREEN_CONSOLE_VISIBLE, top, dir, step))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const bumpSeen = (id: string | null | undefined) => {
|
|
316
|
+
if (!id) return
|
|
317
|
+
setSeen((prev) => ({ ...prev, [id]: Date.now() }))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const apply = (next: RuntimeSnapshot) => {
|
|
321
|
+
setSnap(next)
|
|
322
|
+
requestRender()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Session switch with the tab status row shown until the target snapshot lands. */
|
|
326
|
+
const goSession = (id: string | null | undefined) => {
|
|
327
|
+
if (!id) return
|
|
328
|
+
selectSession(props.api, id)
|
|
329
|
+
if (id === props.sessionId) return
|
|
330
|
+
setSwitching({ id, at: Date.now() })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const projectDir = () => props.api.state.path.directory ?? null
|
|
334
|
+
const projectRoots = () => {
|
|
335
|
+
const dir = projectDir()
|
|
336
|
+
const tree = props.api.state.path.worktree ?? null
|
|
337
|
+
return [dir, tree].filter((p): p is string => Boolean(p))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const bus = createEventBus()
|
|
341
|
+
let watchedId = props.sessionId
|
|
342
|
+
let source = startRuntimeSource({
|
|
343
|
+
bus,
|
|
344
|
+
sessionId: watchedId,
|
|
345
|
+
projectRoot: projectDir(),
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const refresh = () => profile("scan", () => selfTime("scan", () => source.refresh()))
|
|
349
|
+
|
|
350
|
+
const ingestFiles = (hits: FileView[]) => {
|
|
351
|
+
if (!hits.length) return
|
|
352
|
+
setLiveFiles((prev) => {
|
|
353
|
+
const next = { ...prev }
|
|
354
|
+
for (const f of hits) next[f.id] = f
|
|
355
|
+
return next
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const hydrateDiff = () =>
|
|
360
|
+
profile("hydrate", () => {
|
|
361
|
+
try {
|
|
362
|
+
const fn = props.api.client?.session?.diff
|
|
363
|
+
if (typeof fn !== "function") return
|
|
364
|
+
const take = (res: unknown) => {
|
|
365
|
+
const o = res && typeof res === "object" ? (res as Record<string, unknown>) : null
|
|
366
|
+
const rows = o && (Array.isArray(o.data) ? o.data : Array.isArray(o.diff) ? o.diff : null)
|
|
367
|
+
const list = rows ?? (Array.isArray(res) ? res : null)
|
|
368
|
+
if (!list) return
|
|
369
|
+
ingestFiles(
|
|
370
|
+
filesFromEvent(
|
|
371
|
+
{ type: "session.diff", properties: { sessionID: props.sessionId, diff: list } },
|
|
372
|
+
props.sessionId,
|
|
373
|
+
fileFilter(projectDir()),
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
void profileAsync("rpc.diff", () =>
|
|
378
|
+
Promise.resolve()
|
|
379
|
+
.then(() => fn({ sessionID: props.sessionId }))
|
|
380
|
+
.then(take)
|
|
381
|
+
.catch(() => {}),
|
|
382
|
+
)
|
|
383
|
+
} catch {
|
|
384
|
+
// host client without session.diff
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
/** Rebind to the session the props point at. A stale select event only nudges a refresh. */
|
|
389
|
+
const remount = () =>
|
|
390
|
+
profile("remount", () => {
|
|
391
|
+
const id = props.sessionId
|
|
392
|
+
if (id === watchedId) {
|
|
393
|
+
refresh()
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
resetSelfStats()
|
|
397
|
+
watchedId = id
|
|
398
|
+
setLiveTools({})
|
|
399
|
+
setLiveFiles({})
|
|
400
|
+
setTokenTicks([])
|
|
401
|
+
source.setSession(id)
|
|
402
|
+
queueMicrotask(hydrateDiff)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
createEffect(on(() => props.sessionId, remount, { defer: true }))
|
|
406
|
+
|
|
407
|
+
queueMicrotask(hydrateDiff)
|
|
408
|
+
// The monitor's initial forced emit (startRuntimeSource → startMonitor) runs
|
|
409
|
+
// before the EV_OES_SNAPSHOT listener below is registered, so the bus drops
|
|
410
|
+
// it. Re-emit once now that listeners exist — the snapshot was already
|
|
411
|
+
// computed and cached, so this is a cache hit, not a second full read.
|
|
412
|
+
queueMicrotask(refresh)
|
|
413
|
+
|
|
414
|
+
const offSnapshot = bus.on(EV_OES_SNAPSHOT, (evt) => {
|
|
415
|
+
const data = evt.data
|
|
416
|
+
if (!data || typeof data !== "object") return
|
|
417
|
+
const snapshot = (data as Record<string, unknown>).snapshot
|
|
418
|
+
if (!snapshot || typeof snapshot !== "object") return
|
|
419
|
+
apply(snapshot as RuntimeSnapshot)
|
|
420
|
+
const s = switching()
|
|
421
|
+
if (s) {
|
|
422
|
+
const rs = snapshot as RuntimeSnapshot
|
|
423
|
+
const landed = rs.db?.current?.id === s.id
|
|
424
|
+
const failed = Boolean(rs.db?.error) && rs.db?.error !== TAB_STATUS_SESSION_NOT_IN_DB
|
|
425
|
+
if (landed || failed || Date.now() - s.at > SWITCH_TIMEOUT_MS) setSwitching(null)
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const offSessionActivity = bus.on(EV_OC_SESSION_ACTIVITY, (evt) => {
|
|
430
|
+
const data = evt.data
|
|
431
|
+
if (!data || typeof data !== "object") return
|
|
432
|
+
const payload = data as Record<string, unknown>
|
|
433
|
+
const busyFlag = payload.busy
|
|
434
|
+
if (typeof busyFlag !== "boolean") return
|
|
435
|
+
const id = typeof payload.sessionId === "string" && payload.sessionId ? payload.sessionId : props.sessionId
|
|
436
|
+
if (busyFlag) bumpSeen(id)
|
|
437
|
+
setBusy((prev) => ({ ...prev, [id]: busyFlag }))
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const offFlow = bus.on(EV_OC_FLOW, (evt) => {
|
|
441
|
+
const data = evt.data
|
|
442
|
+
if (!data || typeof data !== "object") return
|
|
443
|
+
const payload = data as Record<string, unknown>
|
|
444
|
+
const id = typeof payload.sessionId === "string" ? payload.sessionId : ""
|
|
445
|
+
const dir = payload.dir
|
|
446
|
+
if (!id || (dir !== "recv" && dir !== "wait" && dir !== "tool" && dir !== "clear")) return
|
|
447
|
+
if (dir !== "clear") bumpSeen(id)
|
|
448
|
+
const at = Date.now()
|
|
449
|
+
setFlow((prev) => applyFlow(prev, id, dir, at))
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const offToolHit = bus.on(EV_OC_TOOL_HIT, (evt) => {
|
|
453
|
+
const data = evt.data
|
|
454
|
+
if (!data || typeof data !== "object") return
|
|
455
|
+
const payload = data as Record<string, unknown>
|
|
456
|
+
const hit = payload.hit
|
|
457
|
+
if (!hit || typeof hit !== "object") return
|
|
458
|
+
const h = hit as { id?: unknown }
|
|
459
|
+
if (typeof h.id !== "string" || !h.id) return
|
|
460
|
+
const toolHit = hit as ToolHit
|
|
461
|
+
bumpSeen(toolHit.sessionId ?? props.sessionId)
|
|
462
|
+
setLiveTools((prev) => ({ ...prev, [toolHit.id]: toolHit }))
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const offFilesTouched = bus.on(EV_OC_FILES_TOUCHED, (evt) => {
|
|
466
|
+
const data = evt.data
|
|
467
|
+
if (!data || typeof data !== "object") return
|
|
468
|
+
const payload = data as Record<string, unknown>
|
|
469
|
+
const files = payload.files
|
|
470
|
+
if (!Array.isArray(files)) return
|
|
471
|
+
const sessionId = typeof payload.sessionId === "string" && payload.sessionId ? payload.sessionId : props.sessionId
|
|
472
|
+
bumpSeen(sessionId)
|
|
473
|
+
ingestFiles(files as FileView[])
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const offTokensDelta = bus.on(EV_OC_TOKENS_DELTA, (evt) => {
|
|
477
|
+
const data = evt.data
|
|
478
|
+
if (!data || typeof data !== "object") return
|
|
479
|
+
const payload = data as Record<string, unknown>
|
|
480
|
+
const tokens = payload.tokens
|
|
481
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0) return
|
|
482
|
+
const at = Date.now()
|
|
483
|
+
setTokenTicks((prev) => pushTokenTick(prev, at, tokens, TOKEN_RATE_WINDOW_MS))
|
|
484
|
+
const sessionId = typeof payload.sessionId === "string" && payload.sessionId ? payload.sessionId : props.sessionId
|
|
485
|
+
const kind = payload.kind === "reasoning" ? "reasoning" : "out"
|
|
486
|
+
rtTimeline.ingestEstimate(sessionId, kind, tokens, at)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const offSessionSelect = bus.on(EV_OES_SESSION_SELECT, () => remount())
|
|
490
|
+
|
|
491
|
+
const bridge = startHostEventBridge({
|
|
492
|
+
api: props.api,
|
|
493
|
+
bus,
|
|
494
|
+
sessionId: () => props.sessionId,
|
|
495
|
+
projectRoot: projectDir,
|
|
496
|
+
onRender: requestRender,
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
// One shared realtime timeline (tokens / cache / CPU / RAM). Exact cumulative
|
|
500
|
+
// totals arrive on session.updated; estimated stream deltas arrive on the bus;
|
|
501
|
+
// the UI tick below folds everything into a grid sample every TICK_MS, so the
|
|
502
|
+
// chart's right edge always reaches now instead of waiting for an event.
|
|
503
|
+
const rtTimeline = StatRealtimeTimeline.build(null)
|
|
504
|
+
const rtSampler = EventDriverSampler.create(rtTimeline, (handler) => {
|
|
505
|
+
try {
|
|
506
|
+
const on = props.api.event.on as (
|
|
507
|
+
name: string,
|
|
508
|
+
cb: (evt: unknown, meta?: { directory?: unknown }) => void,
|
|
509
|
+
) => unknown
|
|
510
|
+
const off = on("session.updated", (evt, meta) => {
|
|
511
|
+
const dir = projectDir()
|
|
512
|
+
if (dir && meta?.directory && !samePath(String(meta.directory), dir)) return
|
|
513
|
+
handler(evt)
|
|
514
|
+
})
|
|
515
|
+
return typeof off === "function" ? () => off() : () => {}
|
|
516
|
+
} catch {
|
|
517
|
+
return () => {}
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
rtSampler.start()
|
|
521
|
+
|
|
522
|
+
let tickCount = 0
|
|
523
|
+
const tick = setInterval(() => {
|
|
524
|
+
profile("tick", () => {
|
|
525
|
+
selfTime("tick", () => {
|
|
526
|
+
// Coarse: ages/marks recompute at most once a second; the spinner phase
|
|
527
|
+
// (`frame`) still animates every tick. Returning `prev` skips the cascade.
|
|
528
|
+
setNow((prev) => {
|
|
529
|
+
const n = Date.now()
|
|
530
|
+
return n - prev >= NOW_MS ? n : prev
|
|
531
|
+
})
|
|
532
|
+
setFrame((n) => n + 1)
|
|
533
|
+
// Realtime grid: CPU/RAM are read here and the timeline folds one sample.
|
|
534
|
+
const at = Date.now()
|
|
535
|
+
rtTimeline.ingestCpuRam(readCpuRam(), at)
|
|
536
|
+
rtTimeline.tick(at)
|
|
537
|
+
setRtVersion((n) => n + 1)
|
|
538
|
+
})
|
|
539
|
+
tickCount += 1
|
|
540
|
+
if (selfDiagActive() && tickCount % FPS_READ_EVERY_TICKS === 0) {
|
|
541
|
+
const r = readRendererFps(props.api.renderer)
|
|
542
|
+
setSelfFps(r.fps, r.frameMs)
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
// Periodic floor for the My-work tab light — outside `profile("tick")` so
|
|
546
|
+
// the scan never pollutes the tick budget; snapshot changes rescan sooner.
|
|
547
|
+
maybeScanBadge(MYWORK_BADGE_MS)
|
|
548
|
+
}, TICK_MS)
|
|
549
|
+
|
|
550
|
+
/** Fast glyph heartbeat — spinners and direction flows step at `GLYPH_TICK_MS`. */
|
|
551
|
+
const glyphTick = setInterval(() => setGlyphFrame((n) => n + 1), GLYPH_TICK_MS)
|
|
552
|
+
|
|
553
|
+
const offGit = onGitMarksChange(() => {
|
|
554
|
+
profile("git", () => {
|
|
555
|
+
setGitTick((n) => n + 1)
|
|
556
|
+
queueMicrotask(requestRender)
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
createEffect(() => {
|
|
561
|
+
profile("render", () => {
|
|
562
|
+
frame()
|
|
563
|
+
now()
|
|
564
|
+
flow()
|
|
565
|
+
liveTools()
|
|
566
|
+
liveFiles()
|
|
567
|
+
queueMicrotask(requestRender)
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
createEffect(() => {
|
|
572
|
+
profile("render", () => {
|
|
573
|
+
glyphFrame()
|
|
574
|
+
queueMicrotask(requestRender)
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
// Cold tabs show their loading row for one short beat, then compute for real.
|
|
579
|
+
createEffect(() => {
|
|
580
|
+
const t = coldTab()
|
|
581
|
+
if (!t) return
|
|
582
|
+
const timer = setTimeout(() => setColdTab(null), 100)
|
|
583
|
+
onCleanup(() => clearTimeout(timer))
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
// End the boot bar once the first real snapshot landed after the minimum
|
|
587
|
+
// show time (or the hard ceiling) — then toast exactly once per boot.
|
|
588
|
+
createEffect(() => {
|
|
589
|
+
if (!engaging()) return
|
|
590
|
+
if (engageDone(engageTick(), Boolean(snap().fingerprint))) {
|
|
591
|
+
setEngaging(false)
|
|
592
|
+
engageSeen = true
|
|
593
|
+
try {
|
|
594
|
+
props.api.ui.toast({
|
|
595
|
+
message: "OpenCode Extended Sidebar Loaded. Engage!",
|
|
596
|
+
variant: "success",
|
|
597
|
+
duration: 7000,
|
|
598
|
+
})
|
|
599
|
+
} catch {
|
|
600
|
+
// host without toast
|
|
601
|
+
}
|
|
602
|
+
requestRender()
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
onCleanup(() => {
|
|
607
|
+
clearInterval(tick)
|
|
608
|
+
clearInterval(glyphTick)
|
|
609
|
+
rtSampler.stop()
|
|
610
|
+
bridge.stop()
|
|
611
|
+
offSnapshot()
|
|
612
|
+
offSessionActivity()
|
|
613
|
+
offFlow()
|
|
614
|
+
offToolHit()
|
|
615
|
+
offFilesTouched()
|
|
616
|
+
offTokensDelta()
|
|
617
|
+
offSessionSelect()
|
|
618
|
+
source.stop()
|
|
619
|
+
offGit()
|
|
620
|
+
writeProfileSummary()
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
const rowMark = (
|
|
624
|
+
lifecycle: string | null | undefined,
|
|
625
|
+
archived: boolean,
|
|
626
|
+
isBusy: boolean,
|
|
627
|
+
...stamps: Array<number | null | undefined>
|
|
628
|
+
): AgentMark =>
|
|
629
|
+
composeMark({
|
|
630
|
+
lifecycle,
|
|
631
|
+
archived,
|
|
632
|
+
busy: isBusy,
|
|
633
|
+
ageMs: pulseAgeMs(now(), ...stamps),
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
const mainMark = createMemo(() => {
|
|
637
|
+
const m = snap().db.main
|
|
638
|
+
if (!m) return MARK_QUEUED
|
|
639
|
+
return rowMark(
|
|
640
|
+
m.status === SESSION_STATUS_ARCHIVED ? STATUS_ARCHIVED : null,
|
|
641
|
+
m.status === SESSION_STATUS_ARCHIVED,
|
|
642
|
+
Boolean(busy()[m.id]),
|
|
643
|
+
m.timeUpdated,
|
|
644
|
+
seen()[m.id],
|
|
645
|
+
)
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
const rowFlow = (id: string | null | undefined, isBusy: boolean): FlowDir | null =>
|
|
649
|
+
activeFlow(id ? flow()[id] : undefined, now(), isBusy)
|
|
650
|
+
|
|
651
|
+
const mainFlow = createMemo(() => {
|
|
652
|
+
const m = snap().db.main
|
|
653
|
+
return rowFlow(m?.id, Boolean(m && busy()[m.id]))
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
const currentRow = createMemo(() => snap().db.current)
|
|
657
|
+
|
|
658
|
+
const currentMark = createMemo(() => {
|
|
659
|
+
const c = currentRow()
|
|
660
|
+
if (!c) return MARK_QUEUED
|
|
661
|
+
return rowMark(
|
|
662
|
+
c.status === SESSION_STATUS_ARCHIVED ? STATUS_ARCHIVED : null,
|
|
663
|
+
c.status === SESSION_STATUS_ARCHIVED,
|
|
664
|
+
Boolean(busy()[c.id]),
|
|
665
|
+
c.timeUpdated,
|
|
666
|
+
seen()[c.id],
|
|
667
|
+
)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
const currentFlow = createMemo(() => {
|
|
671
|
+
const c = currentRow()
|
|
672
|
+
return rowFlow(c?.id, Boolean(c && busy()[c.id]))
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
const sessionDelegates = createMemo(() => delegatesForSession(snap(), props.sessionId))
|
|
676
|
+
|
|
677
|
+
const delegateMark = (d: DelegateView): AgentMark =>
|
|
678
|
+
rowMark(
|
|
679
|
+
d.status,
|
|
680
|
+
d.archived,
|
|
681
|
+
Boolean(d.sessionId && busy()[d.sessionId]),
|
|
682
|
+
d.timeUpdated,
|
|
683
|
+
d.sessionId ? seen()[d.sessionId] : null,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
const err = createMemo(() => {
|
|
687
|
+
const e = snap().db.error
|
|
688
|
+
if (!e || e === "session not in db yet") return null
|
|
689
|
+
return e
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
const delegatesLive = createMemo(() => {
|
|
693
|
+
let n = 0
|
|
694
|
+
for (const d of snap().delegates) {
|
|
695
|
+
const m = delegateMark(d)
|
|
696
|
+
if (m === PULSE_LIVE || m === PULSE_STALE) n += 1
|
|
697
|
+
}
|
|
698
|
+
return n
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
const foldAgents = useFold(props.api, KV_FOLD_AGENTS, { after: requestRender })
|
|
702
|
+
const foldDelegates = useFold(props.api, KV_FOLD_DELEGATES, { after: requestRender })
|
|
703
|
+
const foldTools = useFold(props.api, KV_FOLD_TOOLS, { after: requestRender })
|
|
704
|
+
const foldFiles = useFold(props.api, KV_FOLD_FILES, { after: requestRender })
|
|
705
|
+
const foldDrafts = useFold(props.api, KV_FOLD_DRAFTS, { after: requestRender })
|
|
706
|
+
|
|
707
|
+
const myWorkFold = {} as Record<MyWorkKind, ReturnType<typeof useFold>>
|
|
708
|
+
for (const kind of MY_WORK_ORDER) {
|
|
709
|
+
// Errors and Dismissed questions are noise until you want them — start collapsed.
|
|
710
|
+
myWorkFold[kind] = useFold(props.api, `oes.fold.mywork.${kind}`, {
|
|
711
|
+
after: requestRender,
|
|
712
|
+
defaultOpen: kind !== QUESTION_KIND_ERROR && kind !== MY_WORK_GROUP_DISMISSED,
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Reveal state is hoisted, not owned by GroupSection: the My work memos
|
|
717
|
+
// read the `now()` clock, so <For> reconciles them to fresh objects
|
|
718
|
+
// every second — a reveal signal created inside the component would be
|
|
719
|
+
// thrown away with it and the "… +N more" click would collapse on its own.
|
|
720
|
+
const myWorkReveal = {} as Record<MyWorkKind, RevealState>
|
|
721
|
+
for (const kind of MY_WORK_ORDER) {
|
|
722
|
+
myWorkReveal[kind] = useReveal(2)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const oes = () => getOes(projectDir())
|
|
726
|
+
const tools = createMemo(() => mergeTools(snap().db.tools, liveTools(), now(), oes().toolFetch))
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Project-wide feed — tools/files from every main session. The DB queries run
|
|
730
|
+
* only while the Sessions tab is open; elsewhere the memo stays empty.
|
|
731
|
+
*/
|
|
732
|
+
const projectFeed = createMemo<ProjectFeed>(() => {
|
|
733
|
+
if (tab() !== "sessions" || coldTab() === "sessions") return emptyProjectFeed()
|
|
734
|
+
const db = snap().db
|
|
735
|
+
return readProjectFeed({
|
|
736
|
+
dbPath: db.dbPath,
|
|
737
|
+
sessionIds: db.recent.map((s) => s.id),
|
|
738
|
+
toolLimit: oes().toolFetch,
|
|
739
|
+
filter: fileFilter(projectDir()),
|
|
740
|
+
})
|
|
741
|
+
})
|
|
742
|
+
const projectFilesStat = createMemo(() => sumDiff(projectFeed().files))
|
|
743
|
+
|
|
744
|
+
const filesAll = createMemo(() => {
|
|
745
|
+
gitTick()
|
|
746
|
+
return decorateFiles(mergeFiles(snap().db.files ?? [], liveFiles()), projectDir(), {
|
|
747
|
+
git: tab() === "current" && foldFiles.open(),
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
const filesStat = createMemo(() => sumDiff(filesAll()))
|
|
751
|
+
|
|
752
|
+
const pickTab = (next: string) => {
|
|
753
|
+
if (!(OES_TABS as readonly string[]).includes(next)) return
|
|
754
|
+
const picked = next as OesTab
|
|
755
|
+
if (picked !== tab()) {
|
|
756
|
+
setTab(picked)
|
|
757
|
+
kvWriteOne(props.api, KV_TAB, picked)
|
|
758
|
+
// Cold start: the tab's heavy reads run after the status row shows, so
|
|
759
|
+
// the loading line is actually visible instead of a one-frame flash.
|
|
760
|
+
if (picked === "mywork" || picked === "sessions" || picked === "perf") {
|
|
761
|
+
setColdTab(picked)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (picked === "current" || picked === "perf") refresh()
|
|
765
|
+
requestRender()
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const omoPresent = createMemo(() => snap().omo.present)
|
|
769
|
+
|
|
770
|
+
/** Drafts under `.omo/drafts/` that this session wrote — last five inline, the full list behind "view all". */
|
|
771
|
+
const draftsAll = createMemo<DocView[]>(() => {
|
|
772
|
+
if (tab() !== "current" || !omoPresent() || !props.sessionId) return []
|
|
773
|
+
now() // re-scan while open; the docs cache TTL gates the filesystem read
|
|
774
|
+
const db = openReadonlyDb(snap().db.dbPath)
|
|
775
|
+
if (!db) return [] // no writer attribution without the DB
|
|
776
|
+
try {
|
|
777
|
+
return DraftFile.list(projectDir(), props.sessionId, { db })
|
|
778
|
+
} catch (e) {
|
|
779
|
+
dbg("drafts", "error", String(e))
|
|
780
|
+
return []
|
|
781
|
+
}
|
|
782
|
+
})
|
|
783
|
+
const drafts = createMemo(() => draftsAll().slice(0, 5))
|
|
784
|
+
|
|
785
|
+
/** Open `question` tools anywhere in this project — the "answer me" queue. */
|
|
786
|
+
const myWorkQuestions = createMemo<MyWorkItem[]>(() => {
|
|
787
|
+
if (tab() !== "mywork" || coldTab() === "mywork") return []
|
|
788
|
+
try {
|
|
789
|
+
return dropDismissed(
|
|
790
|
+
toQuestionItems(snap().openQuestions),
|
|
791
|
+
readDismissedQuestions(props.api),
|
|
792
|
+
)
|
|
793
|
+
} catch (e) {
|
|
794
|
+
dbg("mywork.questions", "error", String(e))
|
|
795
|
+
return []
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
/** OMO plans/drafts in the action groups, enriched with planner-session state, plus the Draft docs archive. */
|
|
800
|
+
const myWorkApprovals = createMemo<MyWorkItem[]>(() => {
|
|
801
|
+
if (tab() !== "mywork" || coldTab() === "mywork" || !omoPresent()) return []
|
|
802
|
+
now() // re-scan while open; the plans cache TTL gates the filesystem read
|
|
803
|
+
const dir = projectDir()
|
|
804
|
+
try {
|
|
805
|
+
const buckets = listApprovals(dir)
|
|
806
|
+
return [
|
|
807
|
+
...toApprovalItems(
|
|
808
|
+
enrichApprovalSessionStates(
|
|
809
|
+
[
|
|
810
|
+
...buckets.readyReview,
|
|
811
|
+
...buckets.readyStart,
|
|
812
|
+
...buckets.finished,
|
|
813
|
+
...buckets.drafting,
|
|
814
|
+
],
|
|
815
|
+
{
|
|
816
|
+
dbPath: snap().db.dbPath,
|
|
817
|
+
projectRoot: dir,
|
|
818
|
+
},
|
|
819
|
+
),
|
|
820
|
+
),
|
|
821
|
+
// Draft docs / Plans are not queue items — no session enrichment needed.
|
|
822
|
+
...toDraftDocItems(buckets.draftDocs),
|
|
823
|
+
...toPlanItems(buckets.plans),
|
|
824
|
+
]
|
|
825
|
+
} catch (e) {
|
|
826
|
+
dbg("mywork.approvals", "error", String(e))
|
|
827
|
+
return []
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
/** Recent main sessions (running and idle) — the "Sessions" group. */
|
|
832
|
+
const myWorkRunning = createMemo<MyWorkItem[]>(() => {
|
|
833
|
+
if (tab() !== "mywork" || coldTab() === "mywork") return []
|
|
834
|
+
return toSessionItems(snap().db.recent)
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
const myWorkItems = createMemo<MyWorkItem[]>(() => [
|
|
838
|
+
...myWorkQuestions(),
|
|
839
|
+
...myWorkRunning(),
|
|
840
|
+
...myWorkApprovals(),
|
|
841
|
+
])
|
|
842
|
+
|
|
843
|
+
const myWorkGroups = createMemo(() => groupMyWork(myWorkItems()))
|
|
844
|
+
|
|
845
|
+
/** My-work tab light — one scan pipeline for every tab (see `maybeScanBadge`). */
|
|
846
|
+
const [myWorkAttn, setMyWorkAttn] = createSignal<readonly TabAttentionItem[]>([])
|
|
847
|
+
let lastMyWorkScan = 0
|
|
848
|
+
|
|
849
|
+
const scanMyWorkBadge = () => {
|
|
850
|
+
const items: TabAttentionItem[] = []
|
|
851
|
+
try {
|
|
852
|
+
const dismissed = readDismissedQuestions(props.api)
|
|
853
|
+
const open = snap().openQuestions
|
|
854
|
+
for (const q of open) {
|
|
855
|
+
if (dismissed.has(q.partId)) continue
|
|
856
|
+
items.push({ kind: q.kind, ended: q.ended })
|
|
857
|
+
}
|
|
858
|
+
} catch (e) {
|
|
859
|
+
dbg("mywork.badge", "error", String(e))
|
|
860
|
+
}
|
|
861
|
+
if (omoPresent()) {
|
|
862
|
+
try {
|
|
863
|
+
const buckets = listApprovals(projectDir())
|
|
864
|
+
if (buckets.readyReview.length > 0) items.push({ kind: MY_WORK_GROUP_READY_REVIEW })
|
|
865
|
+
if (buckets.readyStart.length > 0) items.push({ kind: MY_WORK_GROUP_READY_START })
|
|
866
|
+
} catch (e) {
|
|
867
|
+
dbg("mywork.badge", "error", String(e))
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
setMyWorkAttn(items)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const maybeScanBadge = (minGapMs: number) => {
|
|
874
|
+
const at = Date.now()
|
|
875
|
+
if (at - lastMyWorkScan < minGapMs) return
|
|
876
|
+
lastMyWorkScan = at
|
|
877
|
+
scanMyWorkBadge()
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const myWorkTabGlyph = createMemo<GlyphSpec>(() => tabAttentionGlyph(myWorkAttn()))
|
|
881
|
+
|
|
882
|
+
createEffect(() => {
|
|
883
|
+
snap()
|
|
884
|
+
maybeScanBadge(MYWORK_BADGE_COOLDOWN_MS)
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Rows are handed out on every render: chrome that always costs a line is
|
|
889
|
+
* counted first, then `packSections` splits what is left.
|
|
890
|
+
*/
|
|
891
|
+
const rowPlan = createMemo(() => {
|
|
892
|
+
try {
|
|
893
|
+
const o = oes()
|
|
894
|
+
const t = tab()
|
|
895
|
+
const omo = omoPresent()
|
|
896
|
+
const sections: { key: string; want: number; min: number; rank: number }[] = []
|
|
897
|
+
let fixed = 2 // OES status line + brand/tabs row
|
|
898
|
+
if (selfDiagActive()) fixed += 1 // self line — only while debug/profile is on
|
|
899
|
+
if (modeLine()) fixed += 1 // debug/profile flag row
|
|
900
|
+
if (modeDirLine()) fixed += 1
|
|
901
|
+
if (engaging()) fixed += 1 // cold-start engage bar row
|
|
902
|
+
fixed += consoleRowSpan() // bottom dbg console — label + visible event rows
|
|
903
|
+
let blocks = 0
|
|
904
|
+
|
|
905
|
+
const header = () => {
|
|
906
|
+
blocks += 1
|
|
907
|
+
fixed += 1
|
|
908
|
+
}
|
|
909
|
+
const section = (folded: boolean, key: string, want: number, min: number, rank: number) => {
|
|
910
|
+
header()
|
|
911
|
+
if (!folded) sections.push({ key, want, min, rank })
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (t === "sessions") {
|
|
915
|
+
header()
|
|
916
|
+
fixed += foldAgents.open() ? 1 : 0
|
|
917
|
+
const feed = projectFeed()
|
|
918
|
+
if (feed.tools.length > 0) section(!foldTools.open(), "tools", o.toolRows, ROW_MIN.tools, ROW_RANK.tools)
|
|
919
|
+
if (feed.files.length > 0) section(!foldFiles.open(), "files", o.fileRows, ROW_MIN.files, ROW_RANK.files)
|
|
920
|
+
if (omo) section(!foldDelegates.open(), "delegates", 6, ROW_MIN.delegates, ROW_RANK.delegates)
|
|
921
|
+
} else if (t === "current") {
|
|
922
|
+
header()
|
|
923
|
+
fixed += foldAgents.open() ? 1 : 0
|
|
924
|
+
section(!foldDelegates.open(), "delegates", 6, ROW_MIN.delegates, ROW_RANK.delegates)
|
|
925
|
+
section(!foldTools.open(), "tools", o.toolRows, ROW_MIN.tools, ROW_RANK.tools)
|
|
926
|
+
section(!foldFiles.open(), "files", o.fileRows, ROW_MIN.files, ROW_RANK.files)
|
|
927
|
+
if (omo && draftsAll().length > 0) {
|
|
928
|
+
section(!foldDrafts.open(), "drafts", Math.min(5, draftsAll().length), ROW_MIN.drafts, ROW_RANK.drafts)
|
|
929
|
+
}
|
|
930
|
+
} else if (t === "mywork") {
|
|
931
|
+
header()
|
|
932
|
+
for (const g of myWorkGroups()) {
|
|
933
|
+
const fold = myWorkFold[g.kind]
|
|
934
|
+
if (!fold) continue
|
|
935
|
+
section(!fold.open(), `mywork.${g.kind}`, g.items.length, ROW_MIN.mywork, ROW_RANK.mywork)
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
// Perf lays out its own sections and caps itself with `perfRows` in
|
|
939
|
+
// its own view; no row-plan sections are handed out here.
|
|
940
|
+
}
|
|
941
|
+
fixed += Math.max(0, blocks - 1) // one blank row between OES sections
|
|
942
|
+
|
|
943
|
+
const height = dimensions()?.height
|
|
944
|
+
return packSections(panelRows(typeof height === "number" ? height : 24), fixed, sections)
|
|
945
|
+
} catch (e) {
|
|
946
|
+
dbg("rowplan", "error", String(e))
|
|
947
|
+
return {}
|
|
948
|
+
}
|
|
949
|
+
}, {})
|
|
950
|
+
|
|
951
|
+
const rowsFor = (key: string, fallback: number): number => {
|
|
952
|
+
try {
|
|
953
|
+
return rowsForPlan(rowPlan(), key, fallback)
|
|
954
|
+
} catch {
|
|
955
|
+
return fallback
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const projectToolsReveal = useReveal(Math.max(1, oes().toolRows))
|
|
960
|
+
const toolsReveal = useReveal(Math.max(1, oes().toolRows))
|
|
961
|
+
const delegateReveal = useReveal(4)
|
|
962
|
+
const currentDelegatesReveal = useReveal(4)
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* A browsable OMO document (Draft docs / Plans archive) opens the same
|
|
966
|
+
* picker the action rows get: Navigate to session first (the session that
|
|
967
|
+
* wrote the file), then the preview row. No Approve — an archive row is
|
|
968
|
+
* never a sign-off queue. Start work is offered only for the Plans
|
|
969
|
+
* archive (via the `startWork` flag); Draft docs stays browse-only.
|
|
970
|
+
*/
|
|
971
|
+
const openDocPicker = (item: { name: string; rel: string }, doc: DocView, docsLabel: string, startWork: boolean = false) => {
|
|
972
|
+
const db = openReadonlyDb(snap().db.dbPath)
|
|
973
|
+
const sessionId = db ? sessionForPlanFile(db, item.rel) : null
|
|
974
|
+
openApprovalDialog(props.api, {
|
|
975
|
+
title: item.name,
|
|
976
|
+
sessionId,
|
|
977
|
+
continueHint: approvalContinueHint(sessionId, Boolean(db)),
|
|
978
|
+
onContinue: (sid) => selectSession(props.api, sid),
|
|
979
|
+
onStartWork: (mode) => runStartWork(props.api, props.sessionId, mode, item.name),
|
|
980
|
+
onDocs: () => openDocDetail(props.api, doc, projectRoots(), colors()),
|
|
981
|
+
showApprove: false,
|
|
982
|
+
showStartWork: startWork,
|
|
983
|
+
docsLabel,
|
|
984
|
+
})
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const myWorkRow = (item: MyWorkItem): RowData => {
|
|
988
|
+
if (item.kind === MY_WORK_GROUP_SESSIONS || item.kind === MY_WORK_GROUP_PINNED) {
|
|
989
|
+
const isBusy = Boolean(item.sessionId && busy()[item.sessionId])
|
|
990
|
+
return {
|
|
991
|
+
kind: ROW_KIND_AGENT,
|
|
992
|
+
mark: rowMark(
|
|
993
|
+
item.status === SESSION_STATUS_ARCHIVED ? STATUS_ARCHIVED : null,
|
|
994
|
+
item.status === SESSION_STATUS_ARCHIVED,
|
|
995
|
+
isBusy,
|
|
996
|
+
item.timeUpdated,
|
|
997
|
+
item.sessionId ? seen()[item.sessionId] : null,
|
|
998
|
+
),
|
|
999
|
+
dirSlot: true,
|
|
1000
|
+
flow: rowFlow(item.sessionId, isBusy),
|
|
1001
|
+
name: item.title,
|
|
1002
|
+
current: item.sessionId === props.sessionId,
|
|
1003
|
+
suffix: item.sessionId === props.sessionId ? "[C]" : undefined,
|
|
1004
|
+
onSelect: () => goSession(item.sessionId),
|
|
1005
|
+
// Pin affordance — placeholder until the pinned backend is wired.
|
|
1006
|
+
link: item.kind === MY_WORK_GROUP_SESSIONS ? { label: "P", onPick: () => {} } : undefined,
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (item.kind === MY_WORK_GROUP_DRAFT_DOCS) {
|
|
1010
|
+
const doc: DocView = {
|
|
1011
|
+
kind: DOC_KIND_DRAFT,
|
|
1012
|
+
name: item.name,
|
|
1013
|
+
rel: item.rel,
|
|
1014
|
+
updatedAt: item.updatedAt,
|
|
1015
|
+
sizeBytes: 0,
|
|
1016
|
+
previewable: true,
|
|
1017
|
+
}
|
|
1018
|
+
return {
|
|
1019
|
+
kind: ROW_KIND_FILE,
|
|
1020
|
+
glyph: myWorkGlyph(item.kind),
|
|
1021
|
+
name: item.name,
|
|
1022
|
+
onSelect: () => openDocPicker(item, doc, "Preview draft file"),
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (item.kind === MY_WORK_GROUP_PLANS) {
|
|
1026
|
+
const doc: DocView = {
|
|
1027
|
+
kind: DOC_KIND_PLAN,
|
|
1028
|
+
name: item.name,
|
|
1029
|
+
rel: item.rel,
|
|
1030
|
+
updatedAt: item.updatedAt,
|
|
1031
|
+
sizeBytes: 0,
|
|
1032
|
+
previewable: true,
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
kind: ROW_KIND_FILE,
|
|
1036
|
+
glyph: myWorkGlyph(item.kind),
|
|
1037
|
+
name: item.name,
|
|
1038
|
+
onSelect: () => openDocPicker(item, doc, "Preview plan file", true),
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if ("sessionId" in item) {
|
|
1042
|
+
const dismissible = item.kind === QUESTION_KIND_INTERRUPTED || item.kind === QUESTION_KIND_ERROR
|
|
1043
|
+
return {
|
|
1044
|
+
kind: ROW_KIND_AGENT,
|
|
1045
|
+
glyph: myWorkGlyph(item.kind),
|
|
1046
|
+
name: item.title,
|
|
1047
|
+
subline: item.reason ?? undefined,
|
|
1048
|
+
onSelect: dismissible
|
|
1049
|
+
? () =>
|
|
1050
|
+
openQuestionDialog(props.api, {
|
|
1051
|
+
title: item.title,
|
|
1052
|
+
sessionId: item.sessionId,
|
|
1053
|
+
onNavigate: (sid) => goSession(sid),
|
|
1054
|
+
onDismiss: () => {
|
|
1055
|
+
dismissQuestion(props.api, item.partId)
|
|
1056
|
+
requestRender()
|
|
1057
|
+
},
|
|
1058
|
+
})
|
|
1059
|
+
: () => goSession(item.sessionId),
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const reviewLabel = reviewStateSuffix(item.review)
|
|
1063
|
+
const drafting = item.kind === MY_WORK_GROUP_DRAFTING
|
|
1064
|
+
const showApprove = item.kind === MY_WORK_GROUP_READY_REVIEW
|
|
1065
|
+
const showStartWork =
|
|
1066
|
+
item.kind === MY_WORK_GROUP_READY_REVIEW || item.kind === MY_WORK_GROUP_READY_START
|
|
1067
|
+
const doc: DocView = {
|
|
1068
|
+
kind: DOC_KIND_DRAFT,
|
|
1069
|
+
name: item.name,
|
|
1070
|
+
rel: item.rel,
|
|
1071
|
+
updatedAt: item.updatedAt,
|
|
1072
|
+
sizeBytes: 0,
|
|
1073
|
+
previewable: true,
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
kind: ROW_KIND_FILE,
|
|
1077
|
+
glyph: myWorkGlyph(item.kind),
|
|
1078
|
+
name: item.name,
|
|
1079
|
+
suffix: reviewLabel || undefined,
|
|
1080
|
+
onSelect: () => {
|
|
1081
|
+
const db = openReadonlyDb(snap().db.dbPath)
|
|
1082
|
+
const sessionId = db ? sessionForPlanFile(db, item.rel) : null
|
|
1083
|
+
const base: Parameters<typeof openApprovalDialog>[1] = {
|
|
1084
|
+
title: item.name,
|
|
1085
|
+
sessionId,
|
|
1086
|
+
continueHint: approvalContinueHint(sessionId, Boolean(db)),
|
|
1087
|
+
onContinue: (sid) => selectSession(props.api, sid),
|
|
1088
|
+
onApprove: (sid) => approvePlan(props.api, sid),
|
|
1089
|
+
onStartWork: (mode) => runStartWork(props.api, props.sessionId, mode, item.name),
|
|
1090
|
+
onDocs: () => openDocDetail(props.api, doc, projectRoots(), colors()),
|
|
1091
|
+
}
|
|
1092
|
+
if (drafting) {
|
|
1093
|
+
openApprovalDialog(props.api, {
|
|
1094
|
+
...base,
|
|
1095
|
+
showApprove: false,
|
|
1096
|
+
showStartWork: false,
|
|
1097
|
+
docsLabel: "Preview plan file",
|
|
1098
|
+
})
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
openApprovalDialog(props.api, { ...base, showApprove, showStartWork })
|
|
1102
|
+
},
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** Perf SQLite scan runs only while this tab is open. */
|
|
1107
|
+
const perf = createMemo(() => {
|
|
1108
|
+
if (tab() !== "perf" || coldTab() === "perf") return emptyPerf(props.sessionId)
|
|
1109
|
+
const o = oes()
|
|
1110
|
+
const history = o.perfHistory > 0
|
|
1111
|
+
? snap()
|
|
1112
|
+
.db.recent.filter((s) => s.id !== props.sessionId)
|
|
1113
|
+
.slice(0, o.perfHistory)
|
|
1114
|
+
.map((s) => ({ id: s.id, title: s.title }))
|
|
1115
|
+
: []
|
|
1116
|
+
return readPerfSnapshot({
|
|
1117
|
+
dbPath: snap().db.dbPath,
|
|
1118
|
+
sessionId: props.sessionId,
|
|
1119
|
+
turns: o.perfTurns,
|
|
1120
|
+
history,
|
|
1121
|
+
cacheKey: `${props.sessionId}::${snap().scanStamp}::${o.perfTurns}`,
|
|
1122
|
+
})
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
const tabStatusFor = (tab: OesTab) =>
|
|
1126
|
+
tabStatus({
|
|
1127
|
+
tab,
|
|
1128
|
+
currentId: snap().db.current?.id ?? null,
|
|
1129
|
+
dbError: snap().db.error,
|
|
1130
|
+
dbPresent: snap().db.present,
|
|
1131
|
+
switching: switching()?.id ?? null,
|
|
1132
|
+
cold: coldTab() === tab,
|
|
1133
|
+
perfError: tab === "perf" ? perf().error : null,
|
|
1134
|
+
perfTurns: tab === "perf" ? perf().totals.turns : 0,
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
const selfFlow = createMemo(() =>
|
|
1138
|
+
rowFlow(props.sessionId, Boolean(busy()[props.sessionId])),
|
|
1139
|
+
)
|
|
1140
|
+
const selfPhaseMs = createMemo(() =>
|
|
1141
|
+
phaseAgeMs(flow()[props.sessionId], now(), selfFlow()),
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
/** The one place that binds a row to theme, frame and the measured row width. */
|
|
1145
|
+
const Row = (row: RowData): JSX.Element => (
|
|
1146
|
+
<AgentLine
|
|
1147
|
+
{...row}
|
|
1148
|
+
lineMax={lineMax()}
|
|
1149
|
+
frame={frame}
|
|
1150
|
+
glyphFrame={glyphFrame}
|
|
1151
|
+
colors={colors()}
|
|
1152
|
+
/>
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
/** Flatten delegates to rows (agent headers + members) for `RowList`. */
|
|
1156
|
+
const delegateLines = (list: DelegateView[]): RowData[] => {
|
|
1157
|
+
const out: RowData[] = []
|
|
1158
|
+
for (const item of groupDelegates(list)) {
|
|
1159
|
+
if (item.kind === "header") {
|
|
1160
|
+
out.push({
|
|
1161
|
+
kind: ROW_KIND_GROUP,
|
|
1162
|
+
glyph: {
|
|
1163
|
+
char: GROUP_GLYPH,
|
|
1164
|
+
tone: markTone(hottestMark(item.members.map(delegateMark))),
|
|
1165
|
+
},
|
|
1166
|
+
name: `${agentDisplayName(item.agent)} (${item.count})`,
|
|
1167
|
+
})
|
|
1168
|
+
continue
|
|
1169
|
+
}
|
|
1170
|
+
const d = item.delegate
|
|
1171
|
+
const isBusy = Boolean(d.sessionId && busy()[d.sessionId])
|
|
1172
|
+
const mark = delegateMark(d)
|
|
1173
|
+
const dir = rowFlow(d.sessionId, isBusy)
|
|
1174
|
+
out.push({
|
|
1175
|
+
kind: item.grouped ? ROW_KIND_DELEGATE : ROW_KIND_AGENT,
|
|
1176
|
+
mark,
|
|
1177
|
+
name: item.grouped ? d.title || d.taskKey || "task" : d.agent || "agent",
|
|
1178
|
+
tokens: d.tokensTotal,
|
|
1179
|
+
title: item.grouped ? undefined : d.title,
|
|
1180
|
+
current: Boolean(d.sessionId && d.sessionId === props.sessionId),
|
|
1181
|
+
flow: dir,
|
|
1182
|
+
dirSlot: true,
|
|
1183
|
+
indent: item.grouped,
|
|
1184
|
+
onSelect: () => goSession(d.sessionId),
|
|
1185
|
+
})
|
|
1186
|
+
}
|
|
1187
|
+
return out
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** A delegate list with its own "… +N more" revealer (state hoisted by the caller). */
|
|
1191
|
+
const DelegateList = (props: {
|
|
1192
|
+
list: DelegateView[]
|
|
1193
|
+
budget: number
|
|
1194
|
+
reveal: RevealState
|
|
1195
|
+
}): JSX.Element => (
|
|
1196
|
+
<RowList
|
|
1197
|
+
items={delegateLines(props.list)}
|
|
1198
|
+
budget={props.budget + props.reveal.more()}
|
|
1199
|
+
colors={colors()}
|
|
1200
|
+
renderItem={(r) => <Row {...r} />}
|
|
1201
|
+
more={{ onReveal: props.reveal.reveal }}
|
|
1202
|
+
/>
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
/** Live token rate from streaming deltas — recomputes on each delta and on the coarse now tick. */
|
|
1206
|
+
const liveTokenRate = createMemo(() => {
|
|
1207
|
+
now()
|
|
1208
|
+
return tokenRate(tokenTicks(), Date.now(), TOKEN_RATE_WINDOW_MS)
|
|
1209
|
+
})
|
|
1210
|
+
const liveRateBar = createMemo(() => {
|
|
1211
|
+
now()
|
|
1212
|
+
return tokenRateBars(tokenTicks(), Date.now(), TOKEN_RATE_WINDOW_MS)
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
/** Live self-cost line — reads the tick clock so the Solid insert re-evaluates every tick. */
|
|
1216
|
+
const selfLine = createMemo(() => {
|
|
1217
|
+
if (!selfDiagActive()) return ""
|
|
1218
|
+
now()
|
|
1219
|
+
return formatSelfLine(readSelfStats())
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
/** "debug mode" / "profile" flags — visible only while the logger is actually active. */
|
|
1223
|
+
const modeLine = () => {
|
|
1224
|
+
const modes: string[] = []
|
|
1225
|
+
if (debugActive()) modes.push("debug mode")
|
|
1226
|
+
if (profileActive()) modes.push("profile")
|
|
1227
|
+
return modes.join(" ")
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const modeDirLine = () => {
|
|
1231
|
+
const dirs = [debugActiveDir(), profileActiveDir()].filter((p): p is string => Boolean(p))
|
|
1232
|
+
if (dirs.length === 0) return ""
|
|
1233
|
+
const uniq = Array.from(new Set(dirs))
|
|
1234
|
+
return `logs ${clipWidth(uniq.join(" | "), 78)}`
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const openRealtimeModal = (): void =>
|
|
1238
|
+
openRealtimeCharts(props.api, colors(), {
|
|
1239
|
+
getTimeline: () => rtTimeline,
|
|
1240
|
+
initialTabId: "tokens",
|
|
1241
|
+
initialRowKey: "avg",
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
return (
|
|
1245
|
+
<box ref={rootRef} flexDirection="column" gap={0}>
|
|
1246
|
+
<Show when={engaging()}>
|
|
1247
|
+
<box flexDirection="row" gap={1}>
|
|
1248
|
+
<text fg={colors().primary}>{`${spinnerFrame(engageTick())} engage`}</text>
|
|
1249
|
+
<text fg={colors().textMuted}>{shareBar(engageFill(engageTick()), 10)}</text>
|
|
1250
|
+
</box>
|
|
1251
|
+
</Show>
|
|
1252
|
+
<Show when={modeLine()}>
|
|
1253
|
+
<text fg={colors().warning || colors().text}>{modeLine()}</text>
|
|
1254
|
+
</Show>
|
|
1255
|
+
<Show when={modeDirLine()}>
|
|
1256
|
+
<text fg={colors().textMuted}>{modeDirLine()}</text>
|
|
1257
|
+
</Show>
|
|
1258
|
+
<box flexDirection="row" gap={1} width="100%">
|
|
1259
|
+
<box flexDirection="row" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
1260
|
+
<OesStatusRow
|
|
1261
|
+
status={tabStatusFor(tab())}
|
|
1262
|
+
colors={colors()}
|
|
1263
|
+
glyphFrame={glyphFrame}
|
|
1264
|
+
rateBar={liveRateBar()}
|
|
1265
|
+
rate={liveTokenRate()}
|
|
1266
|
+
/>
|
|
1267
|
+
</box>
|
|
1268
|
+
<ContextActions
|
|
1269
|
+
actions={[{ label: "C", onPick: openRealtimeModal }]}
|
|
1270
|
+
colors={colors()}
|
|
1271
|
+
width={RT_ACTION_COL_WIDTH}
|
|
1272
|
+
/>
|
|
1273
|
+
</box>
|
|
1274
|
+
<Show when={selfDiagActive()}>
|
|
1275
|
+
<text fg={colors().textMuted}>{selfLine()}</text>
|
|
1276
|
+
</Show>
|
|
1277
|
+
<TabColumn
|
|
1278
|
+
brand=""
|
|
1279
|
+
tabs={OES_TABS}
|
|
1280
|
+
labels={TAB_LABELS}
|
|
1281
|
+
active={tab()}
|
|
1282
|
+
colors={colors()}
|
|
1283
|
+
onPick={pickTab}
|
|
1284
|
+
gap={0}
|
|
1285
|
+
glyph={(key) => (key === "mywork" ? myWorkTabGlyph() : TAB_NEUTRAL_GLYPH)}
|
|
1286
|
+
panels={{
|
|
1287
|
+
mywork: () => {
|
|
1288
|
+
try {
|
|
1289
|
+
const groups = myWorkGroups()
|
|
1290
|
+
dbg("mywork.panel", "render", { groups: groups.map((g) => `${g.kind}:${g.items.length}`) })
|
|
1291
|
+
const status = tabStatusFor("mywork")
|
|
1292
|
+
if (!status && groups.length === 0) return <text fg={colors().textMuted}>• nothing</text>
|
|
1293
|
+
return (
|
|
1294
|
+
<box flexDirection="column" gap={1}>
|
|
1295
|
+
<For each={groups}>
|
|
1296
|
+
{(g) => (
|
|
1297
|
+
<GroupSection
|
|
1298
|
+
title={myWorkLabel(g.kind)}
|
|
1299
|
+
open={myWorkFold[g.kind].open()}
|
|
1300
|
+
onToggle={myWorkFold[g.kind].toggle}
|
|
1301
|
+
colors={colors()}
|
|
1302
|
+
items={g.items}
|
|
1303
|
+
budget={rowsFor(`mywork.${g.kind}`, 2)}
|
|
1304
|
+
reveal={myWorkReveal[g.kind]}
|
|
1305
|
+
actions={
|
|
1306
|
+
g.kind === MY_WORK_GROUP_SESSIONS
|
|
1307
|
+
? [
|
|
1308
|
+
{ label: "switch", onPick: () => openSessionSwitcher(props.api) },
|
|
1309
|
+
{ label: "new", onPick: () => openNewSessionPrompt(props.api) },
|
|
1310
|
+
]
|
|
1311
|
+
: undefined
|
|
1312
|
+
}
|
|
1313
|
+
renderItem={(item) => <Row {...myWorkRow(item)} />}
|
|
1314
|
+
/>
|
|
1315
|
+
)}
|
|
1316
|
+
</For>
|
|
1317
|
+
</box>
|
|
1318
|
+
)
|
|
1319
|
+
} catch (e) {
|
|
1320
|
+
dbg("mywork.panel", "error", String(e))
|
|
1321
|
+
return <text fg={colors().error || colors().text}>• my work error</text>
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
sessions: () => (
|
|
1325
|
+
<box flexDirection="column" gap={1}>
|
|
1326
|
+
<FoldSection title="Agents" open={foldAgents.open()} colors={colors()} onToggle={foldAgents.toggle}>
|
|
1327
|
+
{isRealSession(snap().db.main) ? (
|
|
1328
|
+
<Row
|
|
1329
|
+
kind={ROW_KIND_AGENT}
|
|
1330
|
+
mark={mainMark()}
|
|
1331
|
+
name={snap().db.main!.agent}
|
|
1332
|
+
tokens={snap().db.main!.tokensTotal}
|
|
1333
|
+
cost={snap().db.main!.cost}
|
|
1334
|
+
current={snap().db.main!.id === props.sessionId}
|
|
1335
|
+
flow={mainFlow()}
|
|
1336
|
+
dirSlot
|
|
1337
|
+
onSelect={() => goSession(snap().db.main?.id)}
|
|
1338
|
+
/>
|
|
1339
|
+
) : (
|
|
1340
|
+
<text fg={colors().textMuted}>
|
|
1341
|
+
{`• session · ${props.sessionId.slice(0, 14)}`}
|
|
1342
|
+
{err() ? ` · ${err()}` : ""}
|
|
1343
|
+
</text>
|
|
1344
|
+
)}
|
|
1345
|
+
</FoldSection>
|
|
1346
|
+
|
|
1347
|
+
<Show when={projectFeed().tools.length > 0}>
|
|
1348
|
+
<FoldSection title="Tool Calls" open={foldTools.open()} colors={colors()} onToggle={foldTools.toggle}>
|
|
1349
|
+
<RowList
|
|
1350
|
+
items={projectFeed().tools}
|
|
1351
|
+
budget={rowsFor("tools", oes().toolRows) + projectToolsReveal.more()}
|
|
1352
|
+
colors={colors()}
|
|
1353
|
+
renderItem={(t) => {
|
|
1354
|
+
const mark = toolMark(t.status)
|
|
1355
|
+
const dir = toolFlow(t.status)
|
|
1356
|
+
const dur =
|
|
1357
|
+
t.status === TOOL_STATUS_RUNNING && t.startedAt != null
|
|
1358
|
+
? formatDuration(Math.max(0, now() - t.startedAt))
|
|
1359
|
+
: formatDuration(t.durationMs)
|
|
1360
|
+
return (
|
|
1361
|
+
<Row
|
|
1362
|
+
kind={ROW_KIND_TOOL}
|
|
1363
|
+
mark={mark}
|
|
1364
|
+
name={t.name}
|
|
1365
|
+
suffix={dur}
|
|
1366
|
+
flow={dir}
|
|
1367
|
+
dirSlot
|
|
1368
|
+
onSelect={() => openToolDetail(props.api, t, colors())}
|
|
1369
|
+
/>
|
|
1370
|
+
)
|
|
1371
|
+
}}
|
|
1372
|
+
more={{ onReveal: projectToolsReveal.reveal }}
|
|
1373
|
+
/>
|
|
1374
|
+
</FoldSection>
|
|
1375
|
+
</Show>
|
|
1376
|
+
|
|
1377
|
+
<Show when={projectFeed().files.length > 0}>
|
|
1378
|
+
<FoldSection
|
|
1379
|
+
title="Files"
|
|
1380
|
+
open={foldFiles.open()}
|
|
1381
|
+
count={projectFeed().files.length}
|
|
1382
|
+
diff={projectFilesStat()}
|
|
1383
|
+
actions={[
|
|
1384
|
+
{
|
|
1385
|
+
label: "view all",
|
|
1386
|
+
onPick: () =>
|
|
1387
|
+
openFileListDialog(
|
|
1388
|
+
props.api,
|
|
1389
|
+
"Files",
|
|
1390
|
+
projectFeed().files.map((f) => ({
|
|
1391
|
+
name: f.name,
|
|
1392
|
+
onSelect: () => openFileDetail(props.api, f, projectRoots(), colors()),
|
|
1393
|
+
})),
|
|
1394
|
+
),
|
|
1395
|
+
},
|
|
1396
|
+
]}
|
|
1397
|
+
colors={colors()}
|
|
1398
|
+
onToggle={foldFiles.toggle}
|
|
1399
|
+
>
|
|
1400
|
+
<RowList
|
|
1401
|
+
items={projectFeed().files}
|
|
1402
|
+
budget={rowsFor("files", oes().fileRows)}
|
|
1403
|
+
colors={colors()}
|
|
1404
|
+
renderItem={(f) => (
|
|
1405
|
+
<Row
|
|
1406
|
+
kind={ROW_KIND_FILE}
|
|
1407
|
+
glyph={fileLetterGlyph(f.letter)}
|
|
1408
|
+
name={f.name}
|
|
1409
|
+
diff={{ additions: f.additions, deletions: f.deletions }}
|
|
1410
|
+
onSelect={() => openFileDetail(props.api, f, projectRoots(), colors())}
|
|
1411
|
+
/>
|
|
1412
|
+
)}
|
|
1413
|
+
/>
|
|
1414
|
+
</FoldSection>
|
|
1415
|
+
</Show>
|
|
1416
|
+
|
|
1417
|
+
<Show when={snap().omo.present}>
|
|
1418
|
+
<FoldSection
|
|
1419
|
+
title="Delegates"
|
|
1420
|
+
open={foldDelegates.open()}
|
|
1421
|
+
count={snap().delegates.length}
|
|
1422
|
+
live={delegatesLive()}
|
|
1423
|
+
colors={colors()}
|
|
1424
|
+
onToggle={foldDelegates.toggle}
|
|
1425
|
+
>
|
|
1426
|
+
{snap().delegates.length === 0 ? (
|
|
1427
|
+
<text fg={colors().textMuted}>• none</text>
|
|
1428
|
+
) : (
|
|
1429
|
+
<DelegateList list={snap().delegates} budget={rowsFor("delegates", 6)} reveal={delegateReveal} />
|
|
1430
|
+
)}
|
|
1431
|
+
</FoldSection>
|
|
1432
|
+
</Show>
|
|
1433
|
+
</box>
|
|
1434
|
+
),
|
|
1435
|
+
current: () => (
|
|
1436
|
+
<box flexDirection="column" gap={1}>
|
|
1437
|
+
<FoldSection title="Agents" open={foldAgents.open()} colors={colors()} onToggle={foldAgents.toggle}>
|
|
1438
|
+
{isRealSession(currentRow()) ? (
|
|
1439
|
+
<Row
|
|
1440
|
+
kind={ROW_KIND_AGENT}
|
|
1441
|
+
mark={currentMark()}
|
|
1442
|
+
name={currentRow()!.agent}
|
|
1443
|
+
tokens={currentRow()!.tokensTotal}
|
|
1444
|
+
cost={currentRow()!.cost}
|
|
1445
|
+
current
|
|
1446
|
+
flow={currentFlow()}
|
|
1447
|
+
dirSlot
|
|
1448
|
+
onSelect={() => goSession(currentRow()?.id)}
|
|
1449
|
+
/>
|
|
1450
|
+
) : (
|
|
1451
|
+
<text fg={colors().textMuted}>
|
|
1452
|
+
{`• session · ${props.sessionId.slice(0, 14)}`}
|
|
1453
|
+
</text>
|
|
1454
|
+
)}
|
|
1455
|
+
</FoldSection>
|
|
1456
|
+
|
|
1457
|
+
<Show when={snap().omo.present || sessionDelegates().length > 0}>
|
|
1458
|
+
<FoldSection
|
|
1459
|
+
title="Delegates"
|
|
1460
|
+
open={foldDelegates.open()}
|
|
1461
|
+
count={sessionDelegates().length}
|
|
1462
|
+
live={sessionDelegates().filter((d) => {
|
|
1463
|
+
const m = delegateMark(d)
|
|
1464
|
+
return m === PULSE_LIVE || m === PULSE_STALE
|
|
1465
|
+
}).length}
|
|
1466
|
+
colors={colors()}
|
|
1467
|
+
onToggle={foldDelegates.toggle}
|
|
1468
|
+
>
|
|
1469
|
+
{sessionDelegates().length === 0 ? (
|
|
1470
|
+
<text fg={colors().textMuted}>• none</text>
|
|
1471
|
+
) : (
|
|
1472
|
+
<DelegateList list={sessionDelegates()} budget={rowsFor("delegates", 6)} reveal={currentDelegatesReveal} />
|
|
1473
|
+
)}
|
|
1474
|
+
</FoldSection>
|
|
1475
|
+
</Show>
|
|
1476
|
+
|
|
1477
|
+
<FoldSection title="Tool Calls" open={foldTools.open()} colors={colors()} onToggle={foldTools.toggle}>
|
|
1478
|
+
{tools().length === 0 ? (
|
|
1479
|
+
<text fg={colors().textMuted}>• none</text>
|
|
1480
|
+
) : (
|
|
1481
|
+
<RowList
|
|
1482
|
+
items={tools()}
|
|
1483
|
+
budget={rowsFor("tools", oes().toolRows) + toolsReveal.more()}
|
|
1484
|
+
colors={colors()}
|
|
1485
|
+
renderItem={(t) => {
|
|
1486
|
+
const mark = toolMark(t.status)
|
|
1487
|
+
const dir = toolFlow(t.status)
|
|
1488
|
+
const dur =
|
|
1489
|
+
t.status === TOOL_STATUS_RUNNING && t.startedAt != null
|
|
1490
|
+
? formatDuration(Math.max(0, now() - t.startedAt))
|
|
1491
|
+
: formatDuration(t.durationMs)
|
|
1492
|
+
return (
|
|
1493
|
+
<Row
|
|
1494
|
+
kind={ROW_KIND_TOOL}
|
|
1495
|
+
mark={mark}
|
|
1496
|
+
name={t.name}
|
|
1497
|
+
suffix={dur}
|
|
1498
|
+
flow={dir}
|
|
1499
|
+
dirSlot
|
|
1500
|
+
onSelect={() => openToolDetail(props.api, t, colors())}
|
|
1501
|
+
/>
|
|
1502
|
+
)
|
|
1503
|
+
}}
|
|
1504
|
+
more={{ onReveal: toolsReveal.reveal }}
|
|
1505
|
+
/>
|
|
1506
|
+
)}
|
|
1507
|
+
</FoldSection>
|
|
1508
|
+
|
|
1509
|
+
<FoldSection
|
|
1510
|
+
title="Files"
|
|
1511
|
+
open={foldFiles.open()}
|
|
1512
|
+
count={filesAll().length > 0 ? filesAll().length : undefined}
|
|
1513
|
+
diff={filesStat()}
|
|
1514
|
+
actions={[
|
|
1515
|
+
{
|
|
1516
|
+
label: "view all",
|
|
1517
|
+
onPick: () =>
|
|
1518
|
+
openFileListDialog(
|
|
1519
|
+
props.api,
|
|
1520
|
+
"Files",
|
|
1521
|
+
filesAll().map((f) => ({
|
|
1522
|
+
name: f.name,
|
|
1523
|
+
onSelect: () => openFileDetail(props.api, f, projectRoots(), colors()),
|
|
1524
|
+
})),
|
|
1525
|
+
),
|
|
1526
|
+
},
|
|
1527
|
+
]}
|
|
1528
|
+
colors={colors()}
|
|
1529
|
+
onToggle={foldFiles.toggle}
|
|
1530
|
+
>
|
|
1531
|
+
{filesAll().length === 0 ? (
|
|
1532
|
+
<text fg={colors().textMuted}>• none</text>
|
|
1533
|
+
) : (
|
|
1534
|
+
<RowList
|
|
1535
|
+
items={filesAll()}
|
|
1536
|
+
budget={rowsFor("files", oes().fileRows)}
|
|
1537
|
+
colors={colors()}
|
|
1538
|
+
renderItem={(f) => (
|
|
1539
|
+
<Row
|
|
1540
|
+
kind={ROW_KIND_FILE}
|
|
1541
|
+
glyph={fileLetterGlyph(f.letter)}
|
|
1542
|
+
name={f.name}
|
|
1543
|
+
diff={{ additions: f.additions, deletions: f.deletions }}
|
|
1544
|
+
onSelect={() => openFileDetail(props.api, f, projectRoots(), colors())}
|
|
1545
|
+
/>
|
|
1546
|
+
)}
|
|
1547
|
+
/>
|
|
1548
|
+
)}
|
|
1549
|
+
</FoldSection>
|
|
1550
|
+
|
|
1551
|
+
<Show when={drafts().length > 0}>
|
|
1552
|
+
<FoldSection
|
|
1553
|
+
title="Drafts"
|
|
1554
|
+
open={foldDrafts.open()}
|
|
1555
|
+
count={draftsAll().length}
|
|
1556
|
+
actions={[
|
|
1557
|
+
{
|
|
1558
|
+
label: "view all",
|
|
1559
|
+
onPick: () =>
|
|
1560
|
+
openFileListDialog(
|
|
1561
|
+
props.api,
|
|
1562
|
+
"Drafts",
|
|
1563
|
+
draftsAll().map((d) => ({
|
|
1564
|
+
name: d.name,
|
|
1565
|
+
description: d.rel,
|
|
1566
|
+
onSelect: () => openDocDetail(props.api, d, projectRoots(), colors()),
|
|
1567
|
+
})),
|
|
1568
|
+
),
|
|
1569
|
+
},
|
|
1570
|
+
]}
|
|
1571
|
+
colors={colors()}
|
|
1572
|
+
onToggle={foldDrafts.toggle}
|
|
1573
|
+
>
|
|
1574
|
+
<RowList
|
|
1575
|
+
items={drafts()}
|
|
1576
|
+
budget={rowsFor("drafts", 5)}
|
|
1577
|
+
colors={colors()}
|
|
1578
|
+
renderItem={(d) => {
|
|
1579
|
+
const age = pulseAgeMs(now(), d.updatedAt)
|
|
1580
|
+
return (
|
|
1581
|
+
<Row
|
|
1582
|
+
kind={ROW_KIND_FILE}
|
|
1583
|
+
glyph={{ char: "•", tone: markTone(composeMark({ ageMs: age })) }}
|
|
1584
|
+
name={d.name}
|
|
1585
|
+
suffix={formatAge(age)}
|
|
1586
|
+
onSelect={() => openDocDetail(props.api, d, projectRoots(), colors())}
|
|
1587
|
+
/>
|
|
1588
|
+
)
|
|
1589
|
+
}}
|
|
1590
|
+
/>
|
|
1591
|
+
</FoldSection>
|
|
1592
|
+
</Show>
|
|
1593
|
+
</box>
|
|
1594
|
+
),
|
|
1595
|
+
perf: () => (
|
|
1596
|
+
<box flexDirection="column" gap={1}>
|
|
1597
|
+
<PerfPanel
|
|
1598
|
+
api={props.api}
|
|
1599
|
+
perf={perf()}
|
|
1600
|
+
colors={colors()}
|
|
1601
|
+
lineMax={lineMax()}
|
|
1602
|
+
rows={oes().perfRows}
|
|
1603
|
+
glyphFrame={glyphFrame}
|
|
1604
|
+
livePhase={selfFlow()}
|
|
1605
|
+
livePhaseMs={selfPhaseMs()}
|
|
1606
|
+
currentSessionId={props.sessionId}
|
|
1607
|
+
dbPath={snap().db.dbPath}
|
|
1608
|
+
turns={oes().perfTurns}
|
|
1609
|
+
onSelect={(id) => goSession(id)}
|
|
1610
|
+
/>
|
|
1611
|
+
</box>
|
|
1612
|
+
),
|
|
1613
|
+
}}
|
|
1614
|
+
/>
|
|
1615
|
+
|
|
1616
|
+
{err() && snap().db.main ? (
|
|
1617
|
+
<text fg={colors().textMuted}>{err()}</text>
|
|
1618
|
+
) : null}
|
|
1619
|
+
<Show when={selfDiagActive()}>
|
|
1620
|
+
<box flexDirection="column" gap={0} onMouseScroll={onConsoleScroll}>
|
|
1621
|
+
<text fg={colors().textMuted}>{`dbg · ${consoleLines().length}`}</text>
|
|
1622
|
+
<For each={consoleWindow()}>
|
|
1623
|
+
{(line) => <text fg={colors().textMuted}>{clipWidth(line.text, lineMax())}</text>}
|
|
1624
|
+
</For>
|
|
1625
|
+
</box>
|
|
1626
|
+
</Show>
|
|
1627
|
+
</box>
|
|
1628
|
+
)
|
|
1629
|
+
}
|