@kabmo-labs/kabmo 0.7.0 → 0.9.0
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/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -7,14 +7,11 @@
|
|
|
7
7
|
* The Files panel's test harness: a fake disk, a real realtime server, and the
|
|
8
8
|
* click helpers the panel's specs drive it with.
|
|
9
9
|
*
|
|
10
|
-
* Extracted because
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* The HTTP stub is a real directory model: specs mutate `disk` between reads,
|
|
16
|
-
* which is what makes a refresh assertion fail on a panel that answered from
|
|
17
|
-
* its children cache instead of from the server.
|
|
10
|
+
* Extracted because several spec files exercise the same panel, and each was
|
|
11
|
+
* carrying its own copy of the mount, the HTTP stub and the helpers. The HTTP
|
|
12
|
+
* stub is a real directory model (see `file-explorer-disk.test-util`): specs
|
|
13
|
+
* mutate `disk` between reads, which is what makes a refresh assertion fail on a
|
|
14
|
+
* panel that answered from its children cache instead of from the server.
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
17
|
import { Window } from 'happy-dom';
|
|
@@ -29,14 +26,13 @@ import { startRealtimeTestServer, type RealtimeTestServer } from '@/test-support
|
|
|
29
26
|
import { installDomGlobals, pristineWebSocket, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
30
27
|
import type { TopicResolver } from '@/server/lib/realtime/hub.server';
|
|
31
28
|
import type { GitStatusPayload } from '@/server/lib/fs/git-status-read';
|
|
29
|
+
import { diskHas, entriesOf, moveOnDisk, writeToDisk, type Disk } from '@/test-support/file-explorer-disk.test-util';
|
|
30
|
+
|
|
31
|
+
export type { Disk } from '@/test-support/file-explorer-disk.test-util';
|
|
32
32
|
|
|
33
33
|
export const ROOT = '/ws';
|
|
34
34
|
export const SESSION = 'new-files-panel';
|
|
35
35
|
const TOPIC = fsTopic(`${ROOT}\u0000.`);
|
|
36
|
-
|
|
37
|
-
/** The fake disk: `path -> child -> (null for a folder, 'file' for a file)`. */
|
|
38
|
-
export type Disk = Record<string, Record<string, null | 'file'> | undefined>;
|
|
39
|
-
|
|
40
36
|
const GIT_STATUS: GitStatusPayload = { changes: [], branch: 'main', branches: [], remoteBranches: [] };
|
|
41
37
|
|
|
42
38
|
export interface FileExplorerHarness {
|
|
@@ -46,9 +42,9 @@ export interface FileExplorerHarness {
|
|
|
46
42
|
dirReads: string[];
|
|
47
43
|
/** Paths whose NEXT read answers a non-JSON 500 (a transport failure). */
|
|
48
44
|
failNext: Set<string>;
|
|
49
|
-
/** Every create the panel posted, as its form fields. */
|
|
45
|
+
/** Every create / rename / transfer the panel posted, as its form fields. */
|
|
50
46
|
createRequests: Array<Record<string, string>>;
|
|
51
|
-
|
|
47
|
+
renameRequests: Array<Record<string, string>>;
|
|
52
48
|
transferRequests: Array<Record<string, string>>;
|
|
53
49
|
/** Files the panel asked the editor to open, in order. */
|
|
54
50
|
openedFiles: Array<{ path: string; name: string }>;
|
|
@@ -66,44 +62,36 @@ export interface FileExplorerHarness {
|
|
|
66
62
|
panelBackground: () => HTMLElement;
|
|
67
63
|
/** Right-click the background, leaving its menu open. */
|
|
68
64
|
openMenuOnBackground: () => Promise<void>;
|
|
69
|
-
/** Right-click the background and click
|
|
65
|
+
/** Right-click the background and click its New File / New Folder row. */
|
|
70
66
|
openRootNewFile: () => Promise<void>;
|
|
67
|
+
openRootNewFolder: () => Promise<void>;
|
|
71
68
|
/** Type a name into the open create dialog and submit it. */
|
|
72
69
|
submitNewFile: (name: string) => Promise<void>;
|
|
70
|
+
/** Type a destination into the open rename dialog and submit it. */
|
|
71
|
+
submitRename: (path: string) => Promise<void>;
|
|
72
|
+
renameInput: () => HTMLInputElement | null;
|
|
73
|
+
/** Every row name the tree is currently drawing, in order. */
|
|
74
|
+
visibleNames: () => string[];
|
|
75
|
+
/** The whole rendered document's text — a dialog's inline refusal lives here. */
|
|
76
|
+
documentText: () => string;
|
|
73
77
|
/** Every button label currently in the document — the menu assertions read it. */
|
|
74
78
|
buttonLabels: () => string[];
|
|
75
79
|
/** Click a button by its exact label (a context-menu row, a dialog action). */
|
|
76
80
|
clickMenuRow: (label: string) => Promise<void>;
|
|
77
|
-
/**
|
|
78
|
-
* Drag the row named `from` onto the row named `to`, or onto the panel
|
|
79
|
-
* background when `to` is null — the whole gesture, in the order the browser
|
|
80
|
-
* raises it: dragstart on the source, dragover on the target (where a target
|
|
81
|
-
* refuses), then drop.
|
|
82
|
-
*/
|
|
81
|
+
/** Drag `from` onto `to` (or the background when null): dragstart, dragover, drop. */
|
|
83
82
|
dragRow: (from: string, to: string | null) => Promise<void>;
|
|
84
|
-
/**
|
|
83
|
+
/** DOM globals for the whole file (call from beforeAll / afterAll). */
|
|
85
84
|
install: () => void;
|
|
86
|
-
/** Restore the runner's own globals (call from `afterAll`). */
|
|
87
85
|
restore: () => void;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
/** One directory's entries as the server emits them: folders with `children: null`. */
|
|
91
|
-
function entriesOf(disk: Disk, path: string): unknown[] {
|
|
92
|
-
const dir = disk[path] ?? {};
|
|
93
|
-
return Object.keys(dir).map((child) => {
|
|
94
|
-
const rel = path === '.' ? child : `${path}/${child}`;
|
|
95
|
-
return dir[child] === null
|
|
96
|
-
? { id: rel, name: child, type: 'folder', path: rel, children: null, ignored: false }
|
|
97
|
-
: { id: rel, name: child, type: 'file', path: rel, ignored: false };
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
88
|
export function createFileExplorerHarness(): FileExplorerHarness {
|
|
102
89
|
const harness: FileExplorerHarness = {
|
|
103
90
|
disk: {},
|
|
104
91
|
dirReads: [],
|
|
105
92
|
failNext: new Set(),
|
|
106
93
|
createRequests: [],
|
|
94
|
+
renameRequests: [],
|
|
107
95
|
transferRequests: [],
|
|
108
96
|
openedFiles: [],
|
|
109
97
|
container: null as unknown as HTMLElement,
|
|
@@ -127,6 +115,7 @@ export function createFileExplorerHarness(): FileExplorerHarness {
|
|
|
127
115
|
async mount() {
|
|
128
116
|
harness.failNext = new Set();
|
|
129
117
|
harness.createRequests = [];
|
|
118
|
+
harness.renameRequests = [];
|
|
130
119
|
harness.transferRequests = [];
|
|
131
120
|
harness.openedFiles = [];
|
|
132
121
|
// A fresh mount is a fresh session: the panel's per-session UI state
|
|
@@ -144,8 +133,8 @@ export function createFileExplorerHarness(): FileExplorerHarness {
|
|
|
144
133
|
target.WebSocket = pristineWebSocket;
|
|
145
134
|
target.fetch = async (input: unknown, init?: RequestInit) => {
|
|
146
135
|
const url = String(input);
|
|
147
|
-
const json = (body: unknown) => new Response(JSON.stringify(body), {
|
|
148
|
-
status
|
|
136
|
+
const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), {
|
|
137
|
+
status,
|
|
149
138
|
headers: { 'content-type': 'application/json' },
|
|
150
139
|
});
|
|
151
140
|
if (url.includes('/state')) return json({ sessionId: SESSION, state: {} });
|
|
@@ -161,11 +150,25 @@ export function createFileExplorerHarness(): FileExplorerHarness {
|
|
|
161
150
|
harness.transferRequests.push(fields);
|
|
162
151
|
return json({ success: true, path: `moved-${fields.actionType}` });
|
|
163
152
|
}
|
|
153
|
+
if (fields.actionType === 'rename') {
|
|
154
|
+
harness.renameRequests.push(fields);
|
|
155
|
+
// The real route refuses an existing destination; the disk is the
|
|
156
|
+
// authority here too, so a refusal spec drives the same path.
|
|
157
|
+
const destination = fields.newPath;
|
|
158
|
+
if (destination !== fields.path && diskHas(harness.disk, destination)) {
|
|
159
|
+
return json({ error: `${destination} already exists` }, 409);
|
|
160
|
+
}
|
|
161
|
+
moveOnDisk(harness.disk, fields.path, destination);
|
|
162
|
+
return json({ success: true, path: destination, previousPath: fields.path });
|
|
163
|
+
}
|
|
164
164
|
harness.createRequests.push(fields);
|
|
165
|
-
// The create route answers with the new
|
|
165
|
+
// The create route answers with the new entry's path relative to the
|
|
166
166
|
// LISTED root; the folder is `.` for the panel's own background menu.
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
// `kind` rides back because the tree draws a folder differently from a
|
|
168
|
+
// file, and the request is the only place it is stated.
|
|
169
|
+
const kind = fields.kind === 'folder' ? 'folder' : 'file';
|
|
170
|
+
const rel = writeToDisk(harness.disk, fields.path, fields.name, kind);
|
|
171
|
+
return json({ success: true, kind, path: rel });
|
|
169
172
|
}
|
|
170
173
|
if (url.includes('/api/fs/dir')) {
|
|
171
174
|
const path = new URL(url, 'http://127.0.0.1').searchParams.get('path') ?? '.';
|
|
@@ -241,8 +244,36 @@ export function createFileExplorerHarness(): FileExplorerHarness {
|
|
|
241
244
|
await harness.openMenuOnBackground();
|
|
242
245
|
await harness.clickMenuRow('New File');
|
|
243
246
|
},
|
|
247
|
+
async openRootNewFolder() {
|
|
248
|
+
await harness.openMenuOnBackground();
|
|
249
|
+
await harness.clickMenuRow('New Folder');
|
|
250
|
+
},
|
|
251
|
+
async submitRename(path) {
|
|
252
|
+
const input = harness.renameInput();
|
|
253
|
+
if (!input) throw new Error('no rename dialog');
|
|
254
|
+
await act(async () => {
|
|
255
|
+
input.value = path;
|
|
256
|
+
input.dispatchEvent(new window.Event('input', { bubbles: true }));
|
|
257
|
+
});
|
|
258
|
+
for (let i = 0; i < 5; i += 1) await act(async () => {});
|
|
259
|
+
await harness.clickMenuRow('Rename');
|
|
260
|
+
for (let i = 0; i < 20; i += 1) await act(async () => {});
|
|
261
|
+
},
|
|
262
|
+
renameInput() {
|
|
263
|
+
return document.querySelector('input[aria-label^="Rename "]') as HTMLInputElement | null;
|
|
264
|
+
},
|
|
265
|
+
visibleNames() {
|
|
266
|
+
// A row's name span is the leaf that carries it; the row wrapper reads the
|
|
267
|
+
// same text, so the SPAN is what is collected (one per row).
|
|
268
|
+
return [...harness.container.querySelectorAll('span')]
|
|
269
|
+
.map((el) => el.textContent?.trim() ?? '')
|
|
270
|
+
.filter((text) => text.length > 0);
|
|
271
|
+
},
|
|
272
|
+
documentText() {
|
|
273
|
+
return document.body.textContent ?? '';
|
|
274
|
+
},
|
|
244
275
|
async submitNewFile(name) {
|
|
245
|
-
const input = document.querySelector('input[aria-label
|
|
276
|
+
const input = document.querySelector('input[aria-label^="New "]') as HTMLInputElement | null;
|
|
246
277
|
if (!input) throw new Error('no create dialog');
|
|
247
278
|
await act(async () => {
|
|
248
279
|
input.value = name;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Real loopback listeners and real processes for the lifecycle suites.
|
|
8
|
+
*
|
|
9
|
+
* `probe`, `claimPort` and `findOccupant` all answer questions about sockets
|
|
10
|
+
* and PIDs, so a double would test the double: every case here drives actual
|
|
11
|
+
* listeners on OS-assigned ports and actual processes. The harness exists
|
|
12
|
+
* because those suites are split by subject (the address rules, the health
|
|
13
|
+
* probe, the lock, the claim) and each needs the same six fixtures — a copy per
|
|
14
|
+
* file is how one of them quietly stops covering both loopback families.
|
|
15
|
+
*
|
|
16
|
+
* Two properties are load-bearing:
|
|
17
|
+
*
|
|
18
|
+
* - `localhost` is TWO addresses. A fixture that holds one family and expects
|
|
19
|
+
* the other to read free must first find a port free on both, because the OS
|
|
20
|
+
* assigns per family.
|
|
21
|
+
* - A live PID that is not ours is what a test about ANOTHER instance needs:
|
|
22
|
+
* the guard reads a listener reporting `process.pid` as this very process
|
|
23
|
+
* re-binding (the `bun run --hot` case), which is the opposite of a refusal.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { expect } from 'bun:test';
|
|
27
|
+
import type { Server } from 'bun';
|
|
28
|
+
import net from 'node:net';
|
|
29
|
+
|
|
30
|
+
import { getProcessState } from '@/server/lib/lifecycle/identity';
|
|
31
|
+
import { isPortAvailable, waitForPortFree } from '@/server/lib/lifecycle/probe';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The runner's own fetch, reached through `Bun` so a stub another file left on
|
|
35
|
+
* the global cannot be mistaken for it. Every case here talks to a real
|
|
36
|
+
* listener, and `findOccupant` asks one over `fetch`.
|
|
37
|
+
*/
|
|
38
|
+
export const realFetch = Bun.fetch;
|
|
39
|
+
|
|
40
|
+
async function probeIpv6Loopback(): Promise<boolean> {
|
|
41
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
42
|
+
const probe = net.createServer();
|
|
43
|
+
probe.once('error', () => resolve(false));
|
|
44
|
+
probe.listen({ port: 0, host: '::1' }, () => {
|
|
45
|
+
probe.close(() => resolve(true));
|
|
46
|
+
});
|
|
47
|
+
return await promise;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether this host has a usable IPv6 loopback.
|
|
52
|
+
*
|
|
53
|
+
* A kernel booted without IPv6 answers `EADDRNOTAVAIL` for `::1`, where a
|
|
54
|
+
* case about the two addresses of `localhost` cannot observe anything at all.
|
|
55
|
+
* Skipped BY NAME rather than weakened: an assertion that silently holds on one
|
|
56
|
+
* family would pass while the bug was live.
|
|
57
|
+
*/
|
|
58
|
+
export const ipv6Loopback = await probeIpv6Loopback();
|
|
59
|
+
|
|
60
|
+
export interface LoopbackHarness {
|
|
61
|
+
/** A live listener on an OS-assigned port, plus the number it got. */
|
|
62
|
+
occupy(
|
|
63
|
+
fetch: (request: Request) => Response | Promise<Response>,
|
|
64
|
+
hostname?: string,
|
|
65
|
+
): { server: Server<undefined>; port: number };
|
|
66
|
+
/** A live listener on a specific port and address. */
|
|
67
|
+
occupyAt(port: number, hostname: string, fetch?: (request: Request) => Response): Server<undefined>;
|
|
68
|
+
/** An OS-assigned port, released and confirmed free. */
|
|
69
|
+
freePort(): Promise<number>;
|
|
70
|
+
/** An OS-assigned port free on BOTH loopback families. */
|
|
71
|
+
freePortBothFamilies(): Promise<number>;
|
|
72
|
+
/** A live process that is not this one; killed by `dispose`. */
|
|
73
|
+
livePid(): Promise<number>;
|
|
74
|
+
/** A real process whose argv says `kabmo`, i.e. `matched` to identity.ts. */
|
|
75
|
+
spawnChamberHolder(): Promise<Bun.Subprocess>;
|
|
76
|
+
/** A PID that has exited and been reaped, so it reads as `dead`. */
|
|
77
|
+
deadPid(): Promise<number>;
|
|
78
|
+
/** Stop every listener and kill every child this harness started. */
|
|
79
|
+
dispose(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createLoopbackHarness(): LoopbackHarness {
|
|
83
|
+
const servers: Server<undefined>[] = [];
|
|
84
|
+
const children: Bun.Subprocess[] = [];
|
|
85
|
+
|
|
86
|
+
function occupy(
|
|
87
|
+
fetch: (request: Request) => Response | Promise<Response>,
|
|
88
|
+
hostname = '127.0.0.1',
|
|
89
|
+
): { server: Server<undefined>; port: number } {
|
|
90
|
+
const server = Bun.serve({ port: 0, hostname, fetch }) as Server<undefined>;
|
|
91
|
+
const port = server.port;
|
|
92
|
+
if (port === undefined) throw new Error('the OS did not assign an ephemeral port');
|
|
93
|
+
servers.push(server);
|
|
94
|
+
return { server, port };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function occupyAt(
|
|
98
|
+
port: number,
|
|
99
|
+
hostname: string,
|
|
100
|
+
fetch: (request: Request) => Response = () => new Response('held'),
|
|
101
|
+
): Server<undefined> {
|
|
102
|
+
const server = Bun.serve({ port, hostname, fetch }) as Server<undefined>;
|
|
103
|
+
servers.push(server);
|
|
104
|
+
return server;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function forget(server: Server<undefined>): void {
|
|
108
|
+
const index = servers.indexOf(server);
|
|
109
|
+
if (index >= 0) servers.splice(index, 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function freePort(): Promise<number> {
|
|
113
|
+
// Bun closes a stopped listener's socket on a later event-loop turn, so an
|
|
114
|
+
// immediate check would be a race, not a test.
|
|
115
|
+
const held = occupy(() => new Response('unused'));
|
|
116
|
+
held.server.stop(true);
|
|
117
|
+
forget(held.server);
|
|
118
|
+
if (!(await waitForPortFree(held.port, '127.0.0.1', 2_000, 25))) {
|
|
119
|
+
throw new Error(`ephemeral port ${held.port} never became free`);
|
|
120
|
+
}
|
|
121
|
+
return held.port;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function freePortBothFamilies(): Promise<number> {
|
|
125
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
126
|
+
const candidate = await freePort();
|
|
127
|
+
if (await isPortAvailable(candidate, '::1')) return candidate;
|
|
128
|
+
}
|
|
129
|
+
throw new Error('the OS handed out no ephemeral port free on both loopback families');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function livePid(): Promise<number> {
|
|
133
|
+
const child = Bun.spawn({
|
|
134
|
+
cmd: ['bun', '-e', 'await new Promise(() => {})'],
|
|
135
|
+
stdout: 'ignore',
|
|
136
|
+
stderr: 'ignore',
|
|
137
|
+
});
|
|
138
|
+
children.push(child);
|
|
139
|
+
return child.pid;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function spawnChamberHolder(): Promise<Bun.Subprocess> {
|
|
143
|
+
// The child announces itself before it parks, and that line is what says the
|
|
144
|
+
// OS has finished `exec`: a freshly spawned process's command line is
|
|
145
|
+
// unreadable for the instant between fork and exec, so Linux's probe falls
|
|
146
|
+
// back to `comm` and identity answers `mismatched` for a process that IS
|
|
147
|
+
// ours. Production reads a PID out of a lock file written long before, so it
|
|
148
|
+
// never sees that window — no fixed delay to guess at here.
|
|
149
|
+
const child = Bun.spawn({
|
|
150
|
+
cmd: ['bun', '-e', 'console.log("ready"); await new Promise(() => {})', 'kabmo-holder'],
|
|
151
|
+
stdout: 'pipe',
|
|
152
|
+
stderr: 'ignore',
|
|
153
|
+
});
|
|
154
|
+
children.push(child);
|
|
155
|
+
const reader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
156
|
+
await reader.read();
|
|
157
|
+
reader.releaseLock();
|
|
158
|
+
expect(getProcessState(child.pid)).toBe('matched');
|
|
159
|
+
return child;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function deadPid(): Promise<number> {
|
|
163
|
+
const child = Bun.spawn({ cmd: ['bun', '-e', 'process.exit(0)'], stdout: 'ignore', stderr: 'ignore' });
|
|
164
|
+
const pid = child.pid;
|
|
165
|
+
await child.exited;
|
|
166
|
+
expect(getProcessState(pid)).toBe('dead');
|
|
167
|
+
return pid;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function dispose(): Promise<void> {
|
|
171
|
+
for (const child of children.splice(0)) {
|
|
172
|
+
try {
|
|
173
|
+
child.kill('SIGKILL');
|
|
174
|
+
await child.exited;
|
|
175
|
+
} catch {
|
|
176
|
+
// Already gone.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const server of servers.splice(0)) server.stop(true);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { occupy, occupyAt, freePort, freePortBothFamilies, livePid, spawnChamberHolder, deadPid, dispose };
|
|
183
|
+
}
|