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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "prism-term-core",
"version": "0.40.8",
"version": "0.40.9",
"description": "What Prism Terminal and Prism share: the terminal (pty, shells, agent detection and indicator, themes, links, the panel, dictation) and the update chip with its window. TypeScript source, compiled by the host.",
"license": "MIT",
"private": true,
Expand Down
32 changes: 32 additions & 0 deletions core/renderer/lib/termPathLinks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,35 @@ describe('the folder the tab is in is never a link found from a shown folder', (
expect(findPath('A', CWD, 'Github-loftspace/Stutter')).toBe(null)
})
})

// 2026-10-16: every Claude tab in the owner's Stable copy stopped taking keys
// and scrolls, the page process at 15.9 GB and growing. A tab showing five or
// more folders taught one that had been pushed out of the four kept on every
// paint, and each teaching woke the tab to paint again: forever.
describe('a tab showing many folders settles', () => {
afterEach(() => {
resetPathCache()
resetTermCore()
vi.useRealTimers()
})

it('wakes once per folder, then stops, however many folders it shows', async () => {
vi.useFakeTimers()
const ask = vi.fn(async (_cwd: string, texts: string[]) => texts.map((t) => ({ kind: 'file' as const, abs: t })))
configureTermCore({ api: { termPathKinds: ask } as unknown as TermApi } as TermHostConfig)
const lines = Array.from({ length: 9 }, (_, n) => `see C:\\d${n}\\f.txt and rel\\x.txt`)
let wakes = 0
// The painter: a wake repaints every line, as links.revisit() does.
onPathsFound('A', () => {
wakes += 1
if (wakes < 1000) lines.forEach((l) => linkRanges(l, 'C:\\cwd', 'A'))
})
lines.forEach((l) => linkRanges(l, 'C:\\cwd', 'A'))
for (let i = 0; i < 100; i += 1) await vi.advanceTimersByTimeAsync(50)
const settled = wakes
for (let i = 0; i < 100; i += 1) await vi.advanceTimersByTimeAsync(50)
expect(wakes).toBe(settled)
// One for the answer, one per folder taught: never more.
expect(wakes).toBeLessThanOrEqual(1 + lines.length)
})
})
25 changes: 21 additions & 4 deletions core/renderer/lib/termPathLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ const asked = new Set<string>()
const ANCHORS_MAX = 4
const ANCESTORS = 4
const anchors = new Map<string, string[]>()
/**
* The folders each tab has EVER been taught, for the wake below: a folder
* wakes its tab once, the first time, never again (2026-10-16, the owner's
* Stable copy: every Claude tab stopped taking keys or scrolling, the page
* process at 15.9 GB and growing 2.5 MB/s on 1.5 cores of CPU). The wake used
* to fire whenever a folder was not in the four kept, and a tab showing five
* or more folders pushed one out on every paint, so each repaint taught an
* evicted folder again, woke the tab, and repainted: 2,496 repaints in five
* seconds in the unit test, forever. Capped, so a tab that shows hundreds of
* folders stops waking rather than growing the set without end.
*/
const taught = new Map<string, Set<string>>()
const TAUGHT_MAX = 256

const isRelative = (p: string): boolean => !/^(?:[A-Za-z]:|[\\/]|~)/.test(p)
const hasSeparator = (p: string): boolean => /[\\/]/.test(p)
Expand Down Expand Up @@ -97,11 +110,14 @@ function learnAnchor(owner: string, hit: PathHit): void {
if (!dir) return
const list = anchors.get(owner) ?? []
if (list[0] === dir) return
const next = [dir, ...list.filter((d) => d !== dir)].slice(0, ANCHORS_MAX)
anchors.set(owner, next)
anchors.set(owner, [dir, ...list.filter((d) => d !== dir)].slice(0, ANCHORS_MAX))
// A relative path already painted plain may name something from here: the
// tab looks again, once per new folder, after this paint.
if (!list.includes(dir)) setTimeout(() => listeners.get(owner)?.forEach((fn) => fn()), 0)
// tab looks again after this paint, ONCE per folder in its life (`taught`).
let seen = taught.get(owner)
if (!seen) taught.set(owner, (seen = new Set()))
if (seen.has(dir) || seen.size >= TAUGHT_MAX) return
seen.add(dir)
setTimeout(() => listeners.get(owner)?.forEach((fn) => fn()), 0)
}

/** Where a relative `path` is tried after `cwd`, in order. */
Expand Down Expand Up @@ -274,6 +290,7 @@ export function resetPathCache(): void {
listeners.clear()
asked.clear()
anchors.clear()
taught.clear()
if (timer !== undefined) clearTimeout(timer)
timer = undefined
}
2 changes: 1 addition & 1 deletion core/shared/termVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
* so a core release that bumps `core/package.json` (or xterm) without this
* fails `npm test` (core/README.md, "Releasing the core").
*/
export const TERM_CORE_VERSION = '0.40.8'
export const TERM_CORE_VERSION = '0.40.9'
export const XTERM_VERSION = '6.0.0'
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "prism-terminal",
"productName": "Prism Terminal",
"version": "0.53.0",
"version": "0.53.1",
"description": "A tabbed Windows terminal for AI CLIs.",
"main": "./out/main/index.js",
"author": "Max",
Expand Down
Loading