mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 16:02:42 +00:00
Merge branch 'main' into brennanb2025/sta5698-pty-id-identity
This commit is contained in:
@@ -17,23 +17,59 @@ differ on other Debian-derived releases.
|
||||
|
||||
## Ubuntu and Debian prerequisites
|
||||
|
||||
Install the AppImage runtime dependency and Xvfb:
|
||||
Install the CLI tools, Xvfb, and the shared libraries Electron links against.
|
||||
A minimal server or container image ships none of the Electron libraries, and
|
||||
`orca serve` then fails before Electron starts:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl file jq xvfb zlib1g-dev
|
||||
sudo apt-get install -y \
|
||||
curl file jq xvfb zlib1g-dev ca-certificates git \
|
||||
libgtk-3-0t64 libnss3 libatk1.0-0t64 libatk-bridge2.0-0t64 libgbm1 libasound2t64 \
|
||||
libxtst6 libcups2t64 libdrm2 libxkbcommon0 libpango-1.0-0 libcairo2 libatspi2.0-0t64 \
|
||||
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libxrender1 libx11-xcb1 \
|
||||
libxcb-dri3-0 libxss1
|
||||
```
|
||||
|
||||
On Ubuntu 22.04, install `libfuse2` to execute the AppImage through FUSE. On
|
||||
Ubuntu 24.04 and Debian, the equivalent package may be `libfuse2t64`. FUSE is
|
||||
That command is for Ubuntu 24.04 and newer and Debian 13 and newer. Those
|
||||
releases carried out the 64-bit `time_t` transition, which renamed six of the
|
||||
packages with a `t64` suffix. On Ubuntu 20.04, Ubuntu 22.04, and Debian 12,
|
||||
substitute the unsuffixed names:
|
||||
|
||||
- `libgtk-3-0t64` becomes `libgtk-3-0`
|
||||
- `libatk1.0-0t64` becomes `libatk1.0-0`
|
||||
- `libatk-bridge2.0-0t64` becomes `libatk-bridge2.0-0`
|
||||
- `libasound2t64` becomes `libasound2`
|
||||
- `libcups2t64` becomes `libcups2`
|
||||
- `libatspi2.0-0t64` becomes `libatspi2.0-0`
|
||||
|
||||
The other names are identical on every supported release. The substitution is not
|
||||
symmetric, so use the list that matches the release. A `t64` name on Ubuntu 20.04,
|
||||
Ubuntu 22.04, or Debian 12 fails immediately with `E: Unable to locate package
|
||||
libgtk-3-0t64`. In the other direction the old names mostly still resolve, because
|
||||
each renamed package declares `Provides:` its unsuffixed name — except `libasound2`
|
||||
on Ubuntu 24.04, where `liboss4-salsa-asound2` in `universe` claims that name too.
|
||||
apt will not choose between two providers and exits with `E: Package 'libasound2'
|
||||
has no installation candidate`, which aborts the entire install line and leaves none
|
||||
of the libraries installed.
|
||||
|
||||
On Ubuntu 20.04 and 22.04, install `libfuse2` to execute the AppImage through
|
||||
FUSE. On Ubuntu 24.04 and Debian 13 the package is `libfuse2t64`, though the plain
|
||||
`libfuse2` name also resolves there because nothing else provides it. FUSE is
|
||||
optional: without it, use the AppImage's supported extraction path:
|
||||
|
||||
```bash
|
||||
cd /opt/orca
|
||||
./orca-linux.AppImage --appimage-extract
|
||||
sudo chmod -R a+rX /opt/orca/squashfs-root
|
||||
/opt/orca/squashfs-root/AppRun serve --port 6768
|
||||
```
|
||||
|
||||
The `chmod` is required whenever the extraction runs as a different user than
|
||||
the server: `--appimage-extract` creates `squashfs-root` as `drwx------` owned by
|
||||
the extracting user, so anyone else — including a dedicated service user — cannot
|
||||
even traverse it, and the run fails before Electron starts.
|
||||
|
||||
Docker commonly has no FUSE device. Use `--appimage-extract` once or
|
||||
`--appimage-extract-and-run`; neither requires a privileged container. The
|
||||
extract-and-run wrapper can print extracted paths before Orca starts, so
|
||||
@@ -144,8 +180,15 @@ AppImage, but must not be able to replace it or the rollback artifacts.
|
||||
sudo useradd --system --create-home --shell /usr/sbin/nologin orca
|
||||
sudo chown root:root /opt/orca /opt/orca/orca-linux.AppImage
|
||||
sudo chmod 755 /opt/orca /opt/orca/orca-linux.AppImage
|
||||
# Only if you ran --appimage-extract: extraction leaves squashfs-root root-only.
|
||||
sudo chmod -R a+rX /opt/orca/squashfs-root
|
||||
```
|
||||
|
||||
The last line matters because the two halves of this guide combine badly without
|
||||
it. `--appimage-extract` writes `squashfs-root` as `drwx------ root root`, so the
|
||||
`orca` service user cannot read or traverse the extracted tree and the unit fails
|
||||
at startup. `chmod 755 /opt/orca` alone does not reach into it.
|
||||
|
||||
For most hosts, one `orca serve` service is enough because Orca starts Xvfb on
|
||||
display `:99` when no display exists:
|
||||
|
||||
@@ -834,7 +877,8 @@ refuse to run there and print the command to run on the machine you want.
|
||||
- GPU or DRI warnings on a VPS: keep `LIBGL_ALWAYS_SOFTWARE=1` in the service
|
||||
environment.
|
||||
- Chromium sandbox errors: confirm the service is running as the non-root
|
||||
`orca` user and that `/opt/orca` is readable by that user.
|
||||
`orca` user and that `/opt/orca` is readable by that user, including
|
||||
`/opt/orca/squashfs-root` if you extracted the AppImage.
|
||||
- Clients cannot connect: make sure `--pairing-address` is an address reachable
|
||||
from the client, and make sure firewalls allow the selected `--port`.
|
||||
- Journal shows `Another Orca instance is already running for this userData
|
||||
@@ -857,4 +901,7 @@ profile` and the unit exits `3`: another process already owns the profile, so
|
||||
`sudo systemctl reset-failed orca-serve.service` first.
|
||||
- Diagnosing other missing libraries: extract the AppImage without launching it
|
||||
with `./orca-linux.AppImage --appimage-extract`, then run
|
||||
`ldd squashfs-root/orca` to list any shared libraries the host is missing.
|
||||
`ldd squashfs-root/orca-ide` to list any shared libraries the host is missing.
|
||||
The Electron binary is `orca-ide`, not `orca`; `ldd` on a path that does not
|
||||
exist prints nothing and exits cleanly, which reads as a clean result in
|
||||
exactly the situation where you are hunting a missing library.
|
||||
|
||||
@@ -135,6 +135,13 @@ shape is §7a; key points:
|
||||
- Use the VM image's package manager (`apt`/`dnf`/`apk`, per the base distro — not the provider brand).
|
||||
- Clone with the git token via `GIT_ASKPASS` (§5).
|
||||
- **Trap errors and remove the half-built sandbox** so a crash doesn't leave a paid resource running.
|
||||
- **Never snapshot a machine on which the Orca runtime has already run.** The first `orca serve` creates
|
||||
the runtime's user-data dir, and everything in it gets baked into the image and shared by every VM
|
||||
booted from it: the pairing keypair and device-token registry (`orca-devices.json`,
|
||||
`orca-e2ee-keypair.json`), `agent-session-authority.key`, and the build box's logs, terminal history
|
||||
and orchestration db. Confirmed: two VMs from one such snapshot emitted **identical `deviceToken` and
|
||||
`pairedDeviceId`**. Snapshot **before** the runtime has ever run, or `rm -rf` the whole user-data dir
|
||||
(`~/.config/orca` on Linux) first — deleting a named file list will drift as Orca adds state.
|
||||
- Snapshot the stopped sandbox, parse the snapshot id, and write it + scope/project/port/repo to state.
|
||||
|
||||
---
|
||||
@@ -163,6 +170,10 @@ their own terminal, or via the Claude Code harness bang-prefix (`! <cmd>`, with
|
||||
`!`). You scaffold/boot the sandbox and run steps 3–4, but **you cannot observe the interactive login
|
||||
finishing** — so **ask the user to tell you when it's done** before you verify and re-snapshot.
|
||||
|
||||
This layer inherits §3's rule: if you started `orca serve` on the base or auth sandbox to smoke-test it,
|
||||
delete the runtime's user-data dir (`~/.config/orca` on Linux) before re-snapshotting, or every workspace
|
||||
booted from this image shares one pairing identity and one `agent-session-authority.key`.
|
||||
|
||||
If the agent's credentials are short-lived, warn that the snapshot may need periodic re-auth (§10).
|
||||
|
||||
For disposable runtimes, do **not** treat a host agent config directory (for example `~/.codex`) as the
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -122,6 +122,9 @@ vi.mock('@/store', () => ({
|
||||
(selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
worktreesByRepo: {},
|
||||
settings: {},
|
||||
markdownRichModeSizeOverride: {},
|
||||
setMarkdownRichModeSizeOverride: vi.fn(),
|
||||
openFile: vi.fn(),
|
||||
openMarkdownPreview: vi.fn(),
|
||||
openConflictReviewFile: vi.fn(),
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RICH_MARKDOWN_MAX_SIZE_BYTES } from '../../../../shared/constants'
|
||||
import { formatBytes } from '../status-bar/workspace-space-format'
|
||||
import { MarkdownPreview, RichMarkdownEditor } from './editor-lazy-views'
|
||||
import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit'
|
||||
import { extractFrontMatter, prependFrontMatter } from './markdown-frontmatter'
|
||||
@@ -41,9 +45,11 @@ export function EditorMarkdownFileSurface({
|
||||
handleDirtyStateHint: (dirty: boolean) => void
|
||||
monacoEditor: React.JSX.Element
|
||||
}): React.JSX.Element {
|
||||
const sizeOverridden = useAppStore((s) => s.markdownRichModeSizeOverride[activeFile.id] === true)
|
||||
const setSizeOverride = useAppStore((s) => s.setMarkdownRichModeSizeOverride)
|
||||
const richModeUnsupportedMessage = getMarkdownRichModeUnsupportedMessage(currentContent)
|
||||
const renderMode = getMarkdownRenderMode({
|
||||
exceedsRichModeSizeLimit: exceedsMarkdownRichModeSizeLimit(currentContent),
|
||||
exceedsRichModeSizeLimit: !sizeOverridden && exceedsMarkdownRichModeSizeLimit(currentContent),
|
||||
hasRichModeUnsupportedContent: richModeUnsupportedMessage !== null,
|
||||
viewMode: mdViewMode
|
||||
})
|
||||
@@ -52,13 +58,30 @@ export function EditorMarkdownFileSurface({
|
||||
return <div className="h-full min-h-0">{monacoEditor}</div>
|
||||
}
|
||||
if (renderMode === 'source' && mdViewMode === 'rich') {
|
||||
// Why: only a size fallback is recoverable — unsupported syntax would round-trip badly, so it gets no override.
|
||||
const isSizeFallback = richModeUnsupportedMessage === null
|
||||
const richFallbackMessage =
|
||||
richModeUnsupportedMessage ??
|
||||
'File is too large for rich editing. Showing source mode instead.'
|
||||
translate(
|
||||
'editor.richMarkdown.tooLarge',
|
||||
'File is larger than the {{limit}} rich editing limit. Showing source mode instead.',
|
||||
{ limit: formatBytes(RICH_MARKDOWN_MAX_SIZE_BYTES) }
|
||||
)
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="border-b border-border/60 bg-blue-500/10 px-3 py-2 text-xs text-blue-950 dark:text-blue-100">
|
||||
{richFallbackMessage}
|
||||
<div className="flex items-center gap-3 border-b border-border/60 bg-blue-500/10 px-3 py-2 text-xs text-blue-950 dark:text-blue-100">
|
||||
<span className="min-w-0 flex-1">{richFallbackMessage}</span>
|
||||
{isSizeFallback ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className="shrink-0"
|
||||
onClick={() => setSizeOverride(activeFile.id, true)}
|
||||
>
|
||||
{translate('editor.richMarkdown.openAnyway', 'Open anyway')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 h-full">{monacoEditor}</div>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ function EditorPanelInner({
|
||||
)
|
||||
const markdownViewMode = useAppStore((s) => s.markdownViewMode)
|
||||
const setMarkdownViewMode = useAppStore((s) => s.setMarkdownViewMode)
|
||||
const markdownRichModeSizeOverride = useAppStore((s) => s.markdownRichModeSizeOverride)
|
||||
const editorViewMode = useAppStore((s) => s.editorViewMode)
|
||||
const setEditorViewMode = useAppStore((s) => s.setEditorViewMode)
|
||||
const openFile = useAppStore((s) => s.openFile)
|
||||
@@ -234,6 +235,7 @@ function EditorPanelInner({
|
||||
gitStatusEntries,
|
||||
gitBranchEntries,
|
||||
markdownViewMode,
|
||||
markdownRichModeSizeOverride,
|
||||
isChangesMode,
|
||||
canOpenWorkspaceFileBrowser
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ function renderModel(args: {
|
||||
fileContents?: Record<string, FileContent>
|
||||
editorDrafts?: Record<string, string>
|
||||
markdownViewMode?: Record<string, 'source' | 'rich' | 'preview'>
|
||||
markdownRichModeSizeOverride?: Record<string, boolean>
|
||||
isChangesMode?: boolean
|
||||
gitStatusByWorktree?: Record<string, GitStatusEntry[]>
|
||||
}) {
|
||||
@@ -42,6 +43,7 @@ function renderModel(args: {
|
||||
gitStatusEntries: args.gitStatusByWorktree?.[activeFile.worktreeId],
|
||||
gitBranchEntries: undefined,
|
||||
markdownViewMode: args.markdownViewMode ?? {},
|
||||
markdownRichModeSizeOverride: args.markdownRichModeSizeOverride ?? {},
|
||||
isChangesMode: args.isChangesMode ?? false,
|
||||
canOpenWorkspaceFileBrowser: true
|
||||
})
|
||||
@@ -76,6 +78,7 @@ describe('getEditorPanelRenderModel HTML preview affordance', () => {
|
||||
gitStatusEntries: undefined,
|
||||
gitBranchEntries: undefined,
|
||||
markdownViewMode: {},
|
||||
markdownRichModeSizeOverride: {},
|
||||
isChangesMode: false,
|
||||
canOpenWorkspaceFileBrowser: false
|
||||
})
|
||||
@@ -168,13 +171,35 @@ describe('getEditorPanelRenderModel markdown export affordance', () => {
|
||||
it('disables rich export when a multibyte character crosses the byte limit', () => {
|
||||
const model = renderModel({
|
||||
markdownViewMode: { '/repo/README.md': 'rich' },
|
||||
editorDrafts: { '/repo/README.md': `${'a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES)}\u00e9` }
|
||||
editorDrafts: {
|
||||
'/repo/README.md': `${'a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES)}\u00e9`
|
||||
}
|
||||
})
|
||||
|
||||
expect(model.shouldShowMarkdownExportAction).toBe(true)
|
||||
expect(model.canExportMarkdownToPdf).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps rich mode for an oversized file the user chose to open anyway', () => {
|
||||
const model = renderModel({
|
||||
markdownViewMode: { '/repo/README.md': 'rich' },
|
||||
editorDrafts: { '/repo/README.md': 'a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES + 1) },
|
||||
markdownRichModeSizeOverride: { '/repo/README.md': true }
|
||||
})
|
||||
|
||||
expect(model.canExportMarkdownToPdf).toBe(true)
|
||||
})
|
||||
|
||||
it('scopes the open-anyway override to its own file', () => {
|
||||
const model = renderModel({
|
||||
markdownViewMode: { '/repo/README.md': 'rich' },
|
||||
editorDrafts: { '/repo/README.md': 'a'.repeat(RICH_MARKDOWN_MAX_SIZE_BYTES + 1) },
|
||||
markdownRichModeSizeOverride: { '/repo/OTHER.md': true }
|
||||
})
|
||||
|
||||
expect(model.canExportMarkdownToPdf).toBe(false)
|
||||
})
|
||||
|
||||
it('disables edit export while content is still loading, even with a draft', () => {
|
||||
const model = renderModel({
|
||||
fileContents: {},
|
||||
|
||||
@@ -25,6 +25,7 @@ type EditorPanelRenderModelParams = {
|
||||
gitStatusEntries: StoreState['gitStatusByWorktree'][string] | undefined
|
||||
gitBranchEntries: StoreState['gitBranchChangesByWorktree'][string] | undefined
|
||||
markdownViewMode: StoreState['markdownViewMode']
|
||||
markdownRichModeSizeOverride: StoreState['markdownRichModeSizeOverride']
|
||||
isChangesMode: boolean
|
||||
canOpenWorkspaceFileBrowser: boolean
|
||||
}
|
||||
@@ -36,6 +37,7 @@ export function getEditorPanelRenderModel({
|
||||
gitStatusEntries,
|
||||
gitBranchEntries,
|
||||
markdownViewMode,
|
||||
markdownRichModeSizeOverride,
|
||||
isChangesMode,
|
||||
canOpenWorkspaceFileBrowser
|
||||
}: EditorPanelRenderModelParams) {
|
||||
@@ -130,7 +132,9 @@ export function getEditorPanelRenderModel({
|
||||
const inlineMarkdownRenderMode =
|
||||
activeFile.mode === 'edit' && inlineMarkdownContent !== null
|
||||
? getMarkdownRenderMode({
|
||||
exceedsRichModeSizeLimit: exceedsMarkdownRichModeSizeLimit(inlineMarkdownContent),
|
||||
exceedsRichModeSizeLimit:
|
||||
markdownRichModeSizeOverride[activeFile.id] !== true &&
|
||||
exceedsMarkdownRichModeSizeLimit(inlineMarkdownContent),
|
||||
hasRichModeUnsupportedContent:
|
||||
getMarkdownRichModeUnsupportedMessage(inlineMarkdownContent) !== null,
|
||||
viewMode: mdViewMode
|
||||
|
||||
@@ -100,6 +100,12 @@
|
||||
"findUnavailable": "Find in page is not available while this page streams from the remote host."
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"richMarkdown": {
|
||||
"tooLarge": "File is larger than the {{limit}} rich editing limit. Showing source mode instead.",
|
||||
"openAnyway": "Open anyway"
|
||||
}
|
||||
},
|
||||
"githubChecks": {
|
||||
"retrying": "Retrying…"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createEditorStore } from './editor-slice-test-harness'
|
||||
|
||||
const { toastErrorMock } = vi.hoisted(() => ({
|
||||
toastErrorMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { error: toastErrorMock }
|
||||
}))
|
||||
|
||||
const { notifyHostOfMirroredEditorCloseMock } = vi.hoisted(() => ({
|
||||
notifyHostOfMirroredEditorCloseMock: vi.fn()
|
||||
}))
|
||||
vi.mock('@/runtime/close-mirrored-editor-tab', () => ({
|
||||
notifyHostOfMirroredEditorClose: (...args: unknown[]) =>
|
||||
notifyHostOfMirroredEditorCloseMock(...args)
|
||||
}))
|
||||
|
||||
function openMarkdown(store: ReturnType<typeof createEditorStore>, filePath: string): void {
|
||||
store.getState().openFile({
|
||||
filePath,
|
||||
relativePath: filePath.replace('/repo/', ''),
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
})
|
||||
}
|
||||
|
||||
describe('createEditorSlice rich markdown size override', () => {
|
||||
it('records an opt-in per file and clears it when turned back off', () => {
|
||||
const store = createEditorStore()
|
||||
openMarkdown(store, '/repo/big.md')
|
||||
|
||||
store.getState().setMarkdownRichModeSizeOverride('/repo/big.md', true)
|
||||
expect(store.getState().markdownRichModeSizeOverride).toEqual({ '/repo/big.md': true })
|
||||
|
||||
store.getState().setMarkdownRichModeSizeOverride('/repo/big.md', false)
|
||||
expect(store.getState().markdownRichModeSizeOverride).toEqual({})
|
||||
})
|
||||
|
||||
it('drops the opt-in when the file is closed', async () => {
|
||||
const store = createEditorStore()
|
||||
openMarkdown(store, '/repo/big.md')
|
||||
store.getState().setMarkdownRichModeSizeOverride('/repo/big.md', true)
|
||||
|
||||
await store.getState().closeFile('/repo/big.md')
|
||||
|
||||
expect(store.getState().markdownRichModeSizeOverride).toEqual({})
|
||||
})
|
||||
|
||||
it('drops the opt-in for a replaced preview tab', () => {
|
||||
const store = createEditorStore()
|
||||
|
||||
store.getState().openFile(
|
||||
{
|
||||
filePath: '/repo/docs/README.md',
|
||||
relativePath: 'docs/README.md',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ preview: true }
|
||||
)
|
||||
store.getState().setMarkdownRichModeSizeOverride('/repo/docs/README.md', true)
|
||||
|
||||
store.getState().openFile(
|
||||
{
|
||||
filePath: '/repo/docs/guide.md',
|
||||
relativePath: 'docs/guide.md',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ preview: true }
|
||||
)
|
||||
|
||||
expect(store.getState().markdownRichModeSizeOverride).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -32,6 +32,8 @@ export function createCloseFileAction(
|
||||
delete newEditorDrafts[fileId]
|
||||
const newMarkdownViewMode = { ...s.markdownViewMode }
|
||||
delete newMarkdownViewMode[fileId]
|
||||
const newMarkdownRichModeSizeOverride = { ...s.markdownRichModeSizeOverride }
|
||||
delete newMarkdownRichModeSizeOverride[fileId]
|
||||
const newEditorViewMode = { ...s.editorViewMode }
|
||||
delete newEditorViewMode[fileId]
|
||||
const markdownVisibilityKeys = new Set([fileId])
|
||||
@@ -179,6 +181,7 @@ export function createCloseFileAction(
|
||||
activeFileIdByWorktree: newActiveFileIdByWorktree,
|
||||
activeTabTypeByWorktree: newActiveTabTypeByWorktree,
|
||||
markdownViewMode: newMarkdownViewMode,
|
||||
markdownRichModeSizeOverride: newMarkdownRichModeSizeOverride,
|
||||
editorViewMode: newEditorViewMode,
|
||||
markdownFrontmatterVisible: newMarkdownFrontmatterVisible,
|
||||
markdownTableOfContentsVisible: newMarkdownTableOfContentsVisible,
|
||||
|
||||
@@ -14,6 +14,9 @@ export type EditorDraftState = {
|
||||
clearEditorDrafts: (fileIds: string[]) => void
|
||||
markdownViewMode: Record<string, MarkdownViewMode>
|
||||
setMarkdownViewMode: (fileId: string, mode: MarkdownViewMode) => void
|
||||
// Why: per-file opt-in to open an oversized markdown file in the rich editor despite the size limit.
|
||||
markdownRichModeSizeOverride: Record<string, boolean>
|
||||
setMarkdownRichModeSizeOverride: (fileId: string, enabled: boolean) => void
|
||||
editorViewMode: Record<string, EditorViewMode>
|
||||
setEditorViewMode: (fileId: string, mode: EditorViewMode) => void
|
||||
markdownFrontmatterVisible: Record<string, boolean>
|
||||
@@ -70,6 +73,24 @@ export function createEditorDraftState(set: EditorSet, _get: EditorGet): EditorD
|
||||
markdownViewMode: { ...s.markdownViewMode, [fileId]: mode }
|
||||
})),
|
||||
|
||||
// Rich-markdown size-limit override
|
||||
markdownRichModeSizeOverride: {},
|
||||
setMarkdownRichModeSizeOverride: (fileId, enabled) =>
|
||||
set((s) => {
|
||||
// Why: default is false — delete rather than store it so the record stays minimal.
|
||||
if (!enabled) {
|
||||
if (!(fileId in s.markdownRichModeSizeOverride)) {
|
||||
return s
|
||||
}
|
||||
const next = { ...s.markdownRichModeSizeOverride }
|
||||
delete next[fileId]
|
||||
return { markdownRichModeSizeOverride: next }
|
||||
}
|
||||
return {
|
||||
markdownRichModeSizeOverride: { ...s.markdownRichModeSizeOverride, [fileId]: true }
|
||||
}
|
||||
}),
|
||||
|
||||
// Editor view mode (edit vs changes-diff). See EditorViewMode.
|
||||
editorViewMode: {},
|
||||
setEditorViewMode: (fileId, mode) =>
|
||||
|
||||
@@ -174,6 +174,7 @@ export function applyOpenFileToState(
|
||||
editorDrafts: nextEditorDrafts,
|
||||
editorCursorLine: nextEditorCursorLine,
|
||||
markdownViewMode: nextMarkdownViewMode,
|
||||
markdownRichModeSizeOverride: nextMarkdownRichModeSizeOverride,
|
||||
editorViewMode: nextEditorViewMode,
|
||||
markdownFrontmatterVisible: nextMarkdownFrontmatterVisible,
|
||||
markdownTableOfContentsVisible: nextMarkdownTableOfContentsVisible
|
||||
@@ -235,6 +236,7 @@ export function applyOpenFileToState(
|
||||
editorDrafts: nextEditorDrafts,
|
||||
editorCursorLine: nextEditorCursorLine,
|
||||
markdownViewMode: nextMarkdownViewMode,
|
||||
markdownRichModeSizeOverride: nextMarkdownRichModeSizeOverride,
|
||||
editorViewMode: nextEditorViewMode,
|
||||
markdownFrontmatterVisible: nextMarkdownFrontmatterVisible,
|
||||
markdownTableOfContentsVisible: nextMarkdownTableOfContentsVisible,
|
||||
|
||||
@@ -77,6 +77,7 @@ export function createRecentlyClosedEditorTabs(
|
||||
activeFileId: null,
|
||||
activeTabType: 'terminal',
|
||||
markdownViewMode: {},
|
||||
markdownRichModeSizeOverride: {},
|
||||
editorViewMode: {},
|
||||
markdownFrontmatterVisible: {},
|
||||
markdownTableOfContentsVisible: {},
|
||||
@@ -93,6 +94,11 @@ export function createRecentlyClosedEditorTabs(
|
||||
const newMarkdownViewMode = Object.fromEntries(
|
||||
Object.entries(s.markdownViewMode).filter(([fileId]) => remainingFileIds.has(fileId))
|
||||
)
|
||||
const newMarkdownRichModeSizeOverride = Object.fromEntries(
|
||||
Object.entries(s.markdownRichModeSizeOverride).filter(([fileId]) =>
|
||||
remainingFileIds.has(fileId)
|
||||
)
|
||||
)
|
||||
const newEditorViewMode = Object.fromEntries(
|
||||
Object.entries(s.editorViewMode).filter(([fileId]) => remainingFileIds.has(fileId))
|
||||
)
|
||||
@@ -175,6 +181,7 @@ export function createRecentlyClosedEditorTabs(
|
||||
: s.activeBrowserTabId,
|
||||
activeTabType: browserTabsForWorktree.length > 0 ? 'browser' : 'terminal',
|
||||
markdownViewMode: newMarkdownViewMode,
|
||||
markdownRichModeSizeOverride: newMarkdownRichModeSizeOverride,
|
||||
editorViewMode: newEditorViewMode,
|
||||
markdownFrontmatterVisible: newMarkdownFrontmatterVisible,
|
||||
markdownTableOfContentsVisible: newMarkdownTableOfContentsVisible,
|
||||
|
||||
@@ -117,6 +117,10 @@ export function createRekeyOpenFilesAction(
|
||||
editorDrafts: rekeyFileIdRecord(s.editorDrafts, migrations),
|
||||
editorCursorLine: rekeyFileIdRecord(s.editorCursorLine, migrations),
|
||||
markdownViewMode: rekeyFileIdRecord(s.markdownViewMode, migrations),
|
||||
markdownRichModeSizeOverride: rekeyFileIdRecord(
|
||||
s.markdownRichModeSizeOverride,
|
||||
migrations
|
||||
),
|
||||
editorViewMode: rekeyFileIdRecord(s.editorViewMode, migrations),
|
||||
markdownFrontmatterVisible: rekeyFileIdRecord(s.markdownFrontmatterVisible, migrations),
|
||||
markdownTableOfContentsVisible: rekeyFileIdRecord(
|
||||
|
||||
@@ -205,6 +205,7 @@ export function buildRestoredEditorOwnerTransition(
|
||||
editorDrafts: rekeyFileIdRecord(s.editorDrafts, migrations),
|
||||
editorCursorLine: rekeyFileIdRecord(s.editorCursorLine, migrations),
|
||||
markdownViewMode: rekeyFileIdRecord(s.markdownViewMode, migrations),
|
||||
markdownRichModeSizeOverride: rekeyFileIdRecord(s.markdownRichModeSizeOverride, migrations),
|
||||
editorViewMode: rekeyFileIdRecord(s.editorViewMode, migrations),
|
||||
markdownFrontmatterVisible: rekeyFileIdRecord(s.markdownFrontmatterVisible, migrations),
|
||||
markdownTableOfContentsVisible: rekeyFileIdRecord(
|
||||
|
||||
@@ -77,6 +77,7 @@ export function removeEditorStateForReplacedPreview(
|
||||
| 'editorDrafts'
|
||||
| 'editorCursorLine'
|
||||
| 'markdownViewMode'
|
||||
| 'markdownRichModeSizeOverride'
|
||||
| 'editorViewMode'
|
||||
| 'markdownFrontmatterVisible'
|
||||
| 'markdownTableOfContentsVisible'
|
||||
@@ -89,6 +90,7 @@ export function removeEditorStateForReplacedPreview(
|
||||
| 'editorDrafts'
|
||||
| 'editorCursorLine'
|
||||
| 'markdownViewMode'
|
||||
| 'markdownRichModeSizeOverride'
|
||||
| 'editorViewMode'
|
||||
| 'markdownFrontmatterVisible'
|
||||
| 'markdownTableOfContentsVisible'
|
||||
@@ -110,6 +112,7 @@ export function removeEditorStateForReplacedPreview(
|
||||
editorDrafts: state.editorDrafts,
|
||||
editorCursorLine: state.editorCursorLine,
|
||||
markdownViewMode: state.markdownViewMode,
|
||||
markdownRichModeSizeOverride: state.markdownRichModeSizeOverride,
|
||||
editorViewMode: state.editorViewMode,
|
||||
markdownFrontmatterVisible: state.markdownFrontmatterVisible,
|
||||
markdownTableOfContentsVisible: state.markdownTableOfContentsVisible
|
||||
@@ -125,6 +128,11 @@ export function removeEditorStateForReplacedPreview(
|
||||
markdownViewMode: Object.fromEntries(
|
||||
Object.entries(state.markdownViewMode).filter(([fileId]) => fileId !== replacedFile.id)
|
||||
),
|
||||
markdownRichModeSizeOverride: Object.fromEntries(
|
||||
Object.entries(state.markdownRichModeSizeOverride).filter(
|
||||
([fileId]) => fileId !== replacedFile.id
|
||||
)
|
||||
),
|
||||
editorViewMode: Object.fromEntries(
|
||||
Object.entries(state.editorViewMode).filter(([fileId]) => fileId !== replacedFile.id)
|
||||
),
|
||||
|
||||
@@ -166,6 +166,7 @@ export function createTestStore() {
|
||||
openFiles: [],
|
||||
editorDrafts: {},
|
||||
markdownViewMode: {},
|
||||
markdownRichModeSizeOverride: {},
|
||||
editorViewMode: {},
|
||||
showDotfilesByWorktree: {},
|
||||
expandedDirs: {},
|
||||
|
||||
@@ -116,6 +116,10 @@ export function applyRemoveWorktreeSuccessState(
|
||||
const nextEditorDrafts = removedFileIds.size > 0 ? { ...s.editorDrafts } : s.editorDrafts
|
||||
const nextMarkdownViewMode =
|
||||
removedFileIds.size > 0 ? { ...s.markdownViewMode } : s.markdownViewMode
|
||||
const nextMarkdownRichModeSizeOverride =
|
||||
removedFileIds.size > 0
|
||||
? { ...s.markdownRichModeSizeOverride }
|
||||
: s.markdownRichModeSizeOverride
|
||||
const nextEditorViewMode = removedFileIds.size > 0 ? { ...s.editorViewMode } : s.editorViewMode
|
||||
const nextMarkdownFrontmatterVisible =
|
||||
removedFileIds.size > 0 ? { ...s.markdownFrontmatterVisible } : s.markdownFrontmatterVisible
|
||||
@@ -126,6 +130,7 @@ export function applyRemoveWorktreeSuccessState(
|
||||
for (const fileId of removedFileIds) {
|
||||
delete nextEditorDrafts[fileId]
|
||||
delete nextMarkdownViewMode[fileId]
|
||||
delete nextMarkdownRichModeSizeOverride[fileId]
|
||||
delete nextEditorViewMode[fileId]
|
||||
delete nextMarkdownFrontmatterVisible[fileId]
|
||||
delete nextEditorCursorLine[fileId]
|
||||
@@ -233,6 +238,7 @@ export function applyRemoveWorktreeSuccessState(
|
||||
activeGroupIdByWorktree: nextActiveGroupIdByWorktree,
|
||||
editorDrafts: nextEditorDrafts,
|
||||
markdownViewMode: nextMarkdownViewMode,
|
||||
markdownRichModeSizeOverride: nextMarkdownRichModeSizeOverride,
|
||||
editorViewMode: nextEditorViewMode,
|
||||
markdownFrontmatterVisible: nextMarkdownFrontmatterVisible,
|
||||
editorCursorLine: nextEditorCursorLine,
|
||||
|
||||
@@ -169,6 +169,7 @@ export function buildWorktreePurgeState(
|
||||
// Per-file editor state for removed files
|
||||
editorDrafts: omitByFileId(s.editorDrafts),
|
||||
markdownViewMode: omitByFileId(s.markdownViewMode),
|
||||
markdownRichModeSizeOverride: omitByFileId(s.markdownRichModeSizeOverride),
|
||||
markdownFrontmatterVisible: omitByFileId(s.markdownFrontmatterVisible),
|
||||
// Why: keyed by fileId; the bulk reconcile path previously kept these, leaking a cursor-line / view-mode entry per removed file.
|
||||
editorCursorLine: omitByFileId(s.editorCursorLine),
|
||||
|
||||
@@ -83,7 +83,13 @@ export const getDefaultTerminalRightClickToPaste = (
|
||||
platform = typeof process !== 'undefined' ? process.platform : ''
|
||||
): boolean => platform === 'win32'
|
||||
|
||||
/** Why: ProseMirror's full-document tree lags on large files; above this, fall back to source mode (Monaco). */
|
||||
/** Why: ProseMirror renders the whole document — no virtualization — so cost is
|
||||
* linear in file size and every keystroke re-runs it. Measured in a packaged
|
||||
* build (M-series, `out/`), typing latency and the blocking mount on open:
|
||||
* 100 KB 17 ms / 0 ms · 200 KB 46 ms / 0 ms · 300 KB 84 ms / 1.4 s ·
|
||||
* 600 KB 265 ms / 4.2 s. 300 KB is already the knee, so this is a ceiling to
|
||||
* hold rather than raise; past it, fall back to source mode (Monaco) with a
|
||||
* per-file "Open anyway" escape hatch. Real headroom needs #7056. */
|
||||
export const RICH_MARKDOWN_MAX_SIZE_BYTES = 300 * 1024
|
||||
|
||||
export const DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS = 1000
|
||||
|
||||
Reference in New Issue
Block a user