Skip to content

Commit 7853870

Browse files
committed
fix(ui): preserve settings state and batch perf traces
1 parent 0fe740d commit 7853870

4 files changed

Lines changed: 255 additions & 15 deletions

File tree

packages/ui/src/App.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import { showConfirmDialog } from "./stores/alerts"
88
import InstanceTabs from "./components/instance-tabs"
99
import InstanceShell from "./components/instance/instance-shell2"
1010
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
11-
import { initMarkdown } from "./lib/markdown"
1211
import { initGithubStars } from "./stores/github-stars"
1312

14-
import { useTheme } from "./lib/theme"
1513
import { useCommands } from "./lib/hooks/use-commands"
1614
import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle"
1715
import { getLogger } from "./lib/logger"
@@ -76,7 +74,6 @@ function getInstanceShellCacheLimit() {
7674
const INSTANCE_SHELL_CACHE_LIMIT = getInstanceShellCacheLimit()
7775

7876
const App: Component = () => {
79-
const { isDark } = useTheme()
8077
const { t } = useI18n()
8178
const {
8279
preferences,
@@ -97,6 +94,7 @@ const App: Component = () => {
9794
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
9895
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
9996
const [cachedInstanceIds, setCachedInstanceIds] = createSignal<string[]>([])
97+
const [settingsScreenLoaded, setSettingsScreenLoaded] = createSignal(false)
10098

10199
const phoneQuery = useMediaQuery("(max-width: 767px)")
102100
const isPhoneLayout = createMemo(() => phoneQuery())
@@ -201,10 +199,6 @@ const App: Component = () => {
201199
}
202200
})
203201

204-
createEffect(() => {
205-
void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error))
206-
})
207-
208202
createEffect(() => {
209203
initReleaseNotifications()
210204
})
@@ -235,6 +229,12 @@ const App: Component = () => {
235229
requestAnimationFrame(() => updateInstanceTabBarHeight())
236230
})
237231

232+
createEffect(() => {
233+
if (settingsOpen()) {
234+
setSettingsScreenLoaded(true)
235+
}
236+
})
237+
238238
createEffect(() => {
239239
const currentInstances = instances()
240240
const activeId = activeInstanceId()
@@ -610,7 +610,7 @@ const App: Component = () => {
610610
</div>
611611
</Show>
612612

613-
<Show when={settingsOpen()}>
613+
<Show when={settingsScreenLoaded()}>
614614
<Suspense fallback={null}>
615615
<LazySettingsScreen />
616616
</Suspense>

packages/ui/src/components/settings-screen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const SettingsScreen: Component = () => {
3939
}
4040

4141
return (
42-
<Dialog open={settingsOpen()} onOpenChange={(open) => !open && closeSettings()}>
42+
<Dialog open={settingsOpen()} onOpenChange={(open) => !open && closeSettings()} forceMount>
4343
<Dialog.Portal>
4444
<Dialog.Overlay class="modal-overlay" />
4545
<div class="settings-screen-frame">
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { createLowlight, common } from "lowlight"
2+
3+
// NOTE:
4+
// `@git-diff-view/lowlight` pulls in `createLowlight(all)`, which drags almost the
5+
// whole highlight.js grammar registry into the deferred diff bundle. We intentionally
6+
// switch to `common` here to keep the diff viewer lightweight. Less-common languages
7+
// may fall back to auto-detection/plain highlighting; add targeted registrations here
8+
// if real-world usage shows important gaps.
9+
10+
type AstNode = {
11+
type: string
12+
value?: string
13+
children?: AstNode[]
14+
startIndex?: number
15+
endIndex?: number
16+
lineNumber?: number
17+
}
18+
19+
type SyntaxNodeEntry = {
20+
node: AstNode
21+
wrapper?: AstNode
22+
}
23+
24+
type SyntaxFileLine = {
25+
value: string
26+
lineNumber: number
27+
valueLength: number
28+
nodeList: SyntaxNodeEntry[]
29+
}
30+
31+
type LowlightApi = ReturnType<typeof createLowlight>
32+
33+
export function processAST(ast: { children: AstNode[] }) {
34+
let lineNumber = 1
35+
const syntaxObj: Record<number, SyntaxFileLine> = {}
36+
37+
const loopAST = (nodes: AstNode[], wrapper?: AstNode) => {
38+
nodes.forEach((node) => {
39+
if (node.type === "text") {
40+
const textValue = node.value ?? ""
41+
if (!textValue.includes("\n")) {
42+
const valueLength = textValue.length
43+
if (!syntaxObj[lineNumber]) {
44+
node.startIndex = 0
45+
node.endIndex = valueLength - 1
46+
syntaxObj[lineNumber] = {
47+
value: textValue,
48+
lineNumber,
49+
valueLength,
50+
nodeList: [{ node, wrapper }],
51+
}
52+
} else {
53+
node.startIndex = syntaxObj[lineNumber].valueLength
54+
node.endIndex = node.startIndex + valueLength - 1
55+
syntaxObj[lineNumber].value += textValue
56+
syntaxObj[lineNumber].valueLength += valueLength
57+
syntaxObj[lineNumber].nodeList.push({ node, wrapper })
58+
}
59+
node.lineNumber = lineNumber
60+
return
61+
}
62+
63+
const lines = textValue.split("\n")
64+
node.children = node.children || []
65+
for (let index = 0; index < lines.length; index++) {
66+
const value = index === lines.length - 1 ? lines[index] : `${lines[index]}\n`
67+
const currentLineNumber = index === 0 ? lineNumber : ++lineNumber
68+
const valueLength = value.length
69+
const childNode: AstNode = {
70+
type: "text",
71+
value,
72+
startIndex: Infinity,
73+
endIndex: Infinity,
74+
lineNumber: currentLineNumber,
75+
}
76+
77+
if (!syntaxObj[currentLineNumber]) {
78+
childNode.startIndex = 0
79+
childNode.endIndex = valueLength - 1
80+
syntaxObj[currentLineNumber] = {
81+
value,
82+
lineNumber: currentLineNumber,
83+
valueLength,
84+
nodeList: [{ node: childNode, wrapper }],
85+
}
86+
} else {
87+
childNode.startIndex = syntaxObj[currentLineNumber].valueLength
88+
childNode.endIndex = childNode.startIndex + valueLength - 1
89+
syntaxObj[currentLineNumber].value += value
90+
syntaxObj[currentLineNumber].valueLength += valueLength
91+
syntaxObj[currentLineNumber].nodeList.push({ node: childNode, wrapper })
92+
}
93+
94+
node.children.push(childNode)
95+
}
96+
97+
node.lineNumber = lineNumber
98+
return
99+
}
100+
101+
if (node.children) {
102+
loopAST(node.children, node)
103+
node.lineNumber = lineNumber
104+
}
105+
})
106+
}
107+
108+
loopAST(ast.children)
109+
return { syntaxFileObject: syntaxObj, syntaxFileLineNumber: lineNumber }
110+
}
111+
112+
export function _getAST() {
113+
return {}
114+
}
115+
116+
const lowlight = createLowlight(common)
117+
118+
lowlight.register("vue", function hljsDefineVue(hljs: any) {
119+
return {
120+
subLanguage: "xml",
121+
contains: [
122+
hljs.COMMENT("<!--", "-->", { relevance: 10 }),
123+
{
124+
begin: /^(\s*)(<script>)/gm,
125+
end: /^(\s*)(<\/script>)/gm,
126+
subLanguage: "javascript",
127+
excludeBegin: true,
128+
excludeEnd: true,
129+
},
130+
{
131+
begin: /^(?:\s*)(?:<script\s+lang=(["'])ts\1>)/gm,
132+
end: /^(\s*)(<\/script>)/gm,
133+
subLanguage: "typescript",
134+
excludeBegin: true,
135+
excludeEnd: true,
136+
},
137+
{
138+
begin: /^(\s*)(<style(\s+scoped)?>)/gm,
139+
end: /^(\s*)(<\/style>)/gm,
140+
subLanguage: "css",
141+
excludeBegin: true,
142+
excludeEnd: true,
143+
},
144+
{
145+
begin: /^(?:\s*)(?:<style(?:\s+scoped)?\s+lang=(["'])(?:s[ca]ss)\1(?:\s+scoped)?>)/gm,
146+
end: /^(\s*)(<\/style>)/gm,
147+
subLanguage: "scss",
148+
excludeBegin: true,
149+
excludeEnd: true,
150+
},
151+
{
152+
begin: /^(?:\s*)(?:<style(?:\s+scoped)?\s+lang=(["'])stylus\1(?:\s+scoped)?>)/gm,
153+
end: /^(\s*)(<\/style>)/gm,
154+
subLanguage: "stylus",
155+
excludeBegin: true,
156+
excludeEnd: true,
157+
},
158+
],
159+
}
160+
})
161+
162+
let maxLineToIgnoreSyntax = 2000
163+
const ignoreSyntaxHighlightList: (string | RegExp)[] = []
164+
165+
export const highlighter = {
166+
name: "lowlight",
167+
get maxLineToIgnoreSyntax() {
168+
return maxLineToIgnoreSyntax
169+
},
170+
setMaxLineToIgnoreSyntax(value: number) {
171+
maxLineToIgnoreSyntax = value
172+
},
173+
get ignoreSyntaxHighlightList() {
174+
return ignoreSyntaxHighlightList
175+
},
176+
setIgnoreSyntaxHighlightList(values: (string | RegExp)[]) {
177+
ignoreSyntaxHighlightList.length = 0
178+
ignoreSyntaxHighlightList.push(...values)
179+
},
180+
getAST(raw: string, fileName?: string, lang?: string) {
181+
const language = typeof lang === "string" ? lang.trim() : ""
182+
if (
183+
fileName &&
184+
ignoreSyntaxHighlightList.some((item) => (item instanceof RegExp ? item.test(fileName) : fileName === item))
185+
) {
186+
return undefined
187+
}
188+
189+
if (language && lowlight.registered(language)) {
190+
return lowlight.highlight(language, raw)
191+
}
192+
193+
return lowlight.highlightAuto(raw)
194+
},
195+
processAST(ast: { children: AstNode[] }) {
196+
return processAST(ast)
197+
},
198+
hasRegisteredCurrentLang(lang: string) {
199+
return lowlight.registered(lang)
200+
},
201+
getHighlighterEngine(): LowlightApi {
202+
return lowlight
203+
},
204+
type: "class" as const,
205+
}
206+
207+
export const versions = "local-common"

packages/ui/src/lib/perf.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const TRACE_RETENTION_LIMIT = 200
2828
const TRACE_STALE_AFTER_MS = 10 * 60 * 1000
2929

3030
let inMemoryTrace: PerfTraceEntry[] | null = null
31+
let pendingPersistTrace: PerfTraceEntry[] | null = null
32+
let pendingPersistHandle: number | null = null
3133

3234
function nowMs() {
3335
return typeof performance !== "undefined" ? performance.now() : Date.now()
@@ -64,25 +66,56 @@ function loadTraceFromWindowName(): PerfTraceEntry[] {
6466
}
6567
}
6668

67-
function persistTrace(trace: PerfTraceEntry[]) {
68-
const trimmed = trace.slice(-TRACE_RETENTION_LIMIT)
69-
inMemoryTrace = trimmed
70-
69+
function writeTraceToStorage(trace: PerfTraceEntry[]) {
7170
if (supportsSessionStorage()) {
7271
try {
73-
window.sessionStorage.setItem(TRACE_STORAGE_KEY, JSON.stringify(trimmed))
72+
window.sessionStorage.setItem(TRACE_STORAGE_KEY, JSON.stringify(trace))
7473
} catch {
7574
/* noop */
7675
}
7776
}
7877

7978
if (typeof window !== "undefined") {
8079
try {
81-
window.name = `${WINDOW_NAME_PREFIX}${JSON.stringify(trimmed)}`
80+
window.name = `${WINDOW_NAME_PREFIX}${JSON.stringify(trace)}`
8281
} catch {
8382
/* noop */
8483
}
8584
}
85+
}
86+
87+
function flushTracePersistence() {
88+
pendingPersistHandle = null
89+
if (!pendingPersistTrace) {
90+
return
91+
}
92+
93+
writeTraceToStorage(pendingPersistTrace)
94+
pendingPersistTrace = null
95+
}
96+
97+
function scheduleTracePersistence(trace: PerfTraceEntry[]) {
98+
pendingPersistTrace = trace
99+
if (pendingPersistHandle !== null || typeof window === "undefined") {
100+
return
101+
}
102+
103+
const idleScheduler = (window as Window & {
104+
requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number
105+
}).requestIdleCallback
106+
107+
if (typeof idleScheduler === "function") {
108+
pendingPersistHandle = idleScheduler(() => flushTracePersistence(), { timeout: 250 })
109+
return
110+
}
111+
112+
pendingPersistHandle = window.setTimeout(() => flushTracePersistence(), 50)
113+
}
114+
115+
function persistTrace(trace: PerfTraceEntry[]) {
116+
const trimmed = trace.slice(-TRACE_RETENTION_LIMIT)
117+
inMemoryTrace = trimmed
118+
scheduleTracePersistence(trimmed)
86119

87120
publishPerfHandle(trimmed)
88121
}

0 commit comments

Comments
 (0)