ikanban-web 0.2.10 → 0.2.12
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/dist/assets/{ghostty-web-CkHg_Jfr.js → ghostty-web-CEQVZrGO.js} +1 -1
- package/dist/assets/home-BfGWAM3p.js +1 -0
- package/dist/assets/{index-DbuHW0gF.css → index-7dPULZd1.css} +1 -1
- package/dist/assets/{index-n2BMSLCz.js → index-WJDzUYhg.js} +106 -106
- package/dist/assets/session-BDqUCVwy.js +24 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/app.tsx +82 -61
- package/src/components/dialog-command-palette.tsx +70 -0
- package/src/components/dialog-fork.tsx +1 -1
- package/src/components/dialog-select-file.tsx +1 -1
- package/src/components/dialog-select-server.test.ts +16 -0
- package/src/components/dialog-select-server.tsx +59 -51
- package/src/components/prompt-input/submit.ts +1 -1
- package/src/components/server-list-item-frame.tsx +15 -0
- package/src/components/titlebar.tsx +12 -31
- package/src/context/global-sync/bootstrap-mode.ts +17 -0
- package/src/context/global-sync/bootstrap.ts +18 -10
- package/src/context/global-sync.test.ts +57 -0
- package/src/context/global-sync.tsx +5 -0
- package/src/context/notification.tsx +2 -2
- package/src/i18n/en.ts +13 -13
- package/src/i18n/zh.ts +13 -13
- package/src/pages/directory-layout.tsx +2 -2
- package/src/pages/home.tsx +104 -19
- package/src/pages/layout/sidebar-items.tsx +3 -3
- package/src/pages/layout/sidebar-project.tsx +1 -3
- package/src/pages/layout/sidebar-shell-helpers.ts +1 -1
- package/src/pages/layout/sidebar-shell.test.ts +2 -2
- package/src/pages/layout/sidebar-workspace.tsx +1 -1
- package/src/pages/layout.tsx +89 -179
- package/src/pages/session/message-timeline.tsx +4 -4
- package/src/pages/session/use-session-commands.tsx +1 -1
- package/src/pages/session.tsx +1 -1
- package/dist/assets/home-BTCNns6Z.js +0 -1
- package/dist/assets/session-S93i-VnN.js +0 -24
package/dist/index.html
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
<meta property="og:image" content="/ikanban/social-share.png" />
|
|
28
28
|
<meta property="twitter:image" content="/social-share.png" />
|
|
29
29
|
<script id="oc-theme-preload-script" src="/ikanban/oc-theme-preload.js"></script>
|
|
30
|
-
<script type="module" crossorigin src="/ikanban/assets/index-
|
|
31
|
-
<link rel="stylesheet" crossorigin href="/ikanban/assets/index-
|
|
30
|
+
<script type="module" crossorigin src="/ikanban/assets/index-WJDzUYhg.js"></script>
|
|
31
|
+
<link rel="stylesheet" crossorigin href="/ikanban/assets/index-7dPULZd1.css">
|
|
32
32
|
</head>
|
|
33
33
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
|
34
34
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -1,43 +1,54 @@
|
|
|
1
|
-
import "@/index.css"
|
|
2
|
-
import { File } from "ikanban-ui/file"
|
|
3
|
-
import { I18nProvider } from "ikanban-ui/context"
|
|
4
|
-
import { DialogProvider } from "ikanban-ui/context/dialog"
|
|
5
|
-
import { FileComponentProvider } from "ikanban-ui/context/file"
|
|
6
|
-
import { MarkedProvider } from "ikanban-ui/context/marked"
|
|
7
|
-
import { Font } from "ikanban-ui/font"
|
|
8
|
-
import { ThemeProvider } from "ikanban-ui/theme"
|
|
9
|
-
import { MetaProvider } from "@solidjs/meta"
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
1
|
+
import "@/index.css";
|
|
2
|
+
import { File } from "ikanban-ui/file";
|
|
3
|
+
import { I18nProvider } from "ikanban-ui/context";
|
|
4
|
+
import { DialogProvider } from "ikanban-ui/context/dialog";
|
|
5
|
+
import { FileComponentProvider } from "ikanban-ui/context/file";
|
|
6
|
+
import { MarkedProvider } from "ikanban-ui/context/marked";
|
|
7
|
+
import { Font } from "ikanban-ui/font";
|
|
8
|
+
import { ThemeProvider } from "ikanban-ui/theme";
|
|
9
|
+
import { MetaProvider } from "@solidjs/meta";
|
|
10
|
+
import { Route, Router } from "@solidjs/router";
|
|
11
|
+
import {
|
|
12
|
+
ErrorBoundary,
|
|
13
|
+
type JSX,
|
|
14
|
+
lazy,
|
|
15
|
+
type ParentProps,
|
|
16
|
+
Show,
|
|
17
|
+
Suspense,
|
|
18
|
+
} from "solid-js";
|
|
19
|
+
import { CommandProvider } from "@/context/command";
|
|
20
|
+
import { CommentsProvider } from "@/context/comments";
|
|
21
|
+
import { FileProvider } from "@/context/file";
|
|
22
|
+
import { GlobalSDKProvider } from "@/context/global-sdk";
|
|
23
|
+
import { GlobalSyncProvider } from "@/context/global-sync";
|
|
24
|
+
import { HighlightsProvider } from "@/context/highlights";
|
|
25
|
+
import { LanguageProvider, useLanguage } from "@/context/language";
|
|
26
|
+
import { LayoutProvider } from "@/context/layout";
|
|
27
|
+
import { ModelsProvider } from "@/context/models";
|
|
28
|
+
import { NotificationProvider } from "@/context/notification";
|
|
29
|
+
import { PermissionProvider } from "@/context/permission";
|
|
30
|
+
import { usePlatform } from "@/context/platform";
|
|
31
|
+
import { PromptProvider } from "@/context/prompt";
|
|
32
|
+
import {
|
|
33
|
+
type ServerConnection,
|
|
34
|
+
ServerProvider,
|
|
35
|
+
useServer,
|
|
36
|
+
} from "@/context/server";
|
|
37
|
+
import { SettingsProvider } from "@/context/settings";
|
|
38
|
+
import { TerminalProvider } from "@/context/terminal";
|
|
39
|
+
import DirectoryLayout from "@/pages/directory-layout";
|
|
40
|
+
import Layout from "@/pages/layout";
|
|
41
|
+
import { ErrorPage } from "./pages/error";
|
|
31
42
|
|
|
32
|
-
const Home = lazy(() => import("@/pages/home"))
|
|
33
|
-
const Session = lazy(() => import("@/pages/session"))
|
|
34
|
-
const Loading = () => <div class="size-full"
|
|
43
|
+
const Home = lazy(() => import("@/pages/home"));
|
|
44
|
+
const Session = lazy(() => import("@/pages/session"));
|
|
45
|
+
const Loading = () => <div class="size-full" />;
|
|
35
46
|
|
|
36
47
|
const HomeRoute = () => (
|
|
37
48
|
<Suspense fallback={<Loading />}>
|
|
38
49
|
<Home />
|
|
39
50
|
</Suspense>
|
|
40
|
-
)
|
|
51
|
+
);
|
|
41
52
|
|
|
42
53
|
const SessionRoute = () => (
|
|
43
54
|
<SessionProviders>
|
|
@@ -45,32 +56,36 @@ const SessionRoute = () => (
|
|
|
45
56
|
<Session />
|
|
46
57
|
</Suspense>
|
|
47
58
|
</SessionProviders>
|
|
48
|
-
)
|
|
59
|
+
);
|
|
49
60
|
|
|
50
|
-
const SessionIndexRoute = () =>
|
|
51
|
-
<Suspense fallback={<Loading />}>
|
|
52
|
-
<Home />
|
|
53
|
-
</Suspense>
|
|
54
|
-
)
|
|
61
|
+
const SessionIndexRoute = () => <SessionRoute />;
|
|
55
62
|
|
|
56
63
|
function UiI18nBridge(props: ParentProps) {
|
|
57
|
-
const language = useLanguage()
|
|
58
|
-
return
|
|
64
|
+
const language = useLanguage();
|
|
65
|
+
return (
|
|
66
|
+
<I18nProvider value={{ locale: language.locale, t: language.t }}>
|
|
67
|
+
{props.children}
|
|
68
|
+
</I18nProvider>
|
|
69
|
+
);
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
declare global {
|
|
62
73
|
interface Window {
|
|
63
74
|
__OPENCODE__?: {
|
|
64
|
-
updaterEnabled?: boolean
|
|
65
|
-
deepLinks?: string[]
|
|
66
|
-
wsl?: boolean
|
|
67
|
-
}
|
|
75
|
+
updaterEnabled?: boolean;
|
|
76
|
+
deepLinks?: string[];
|
|
77
|
+
wsl?: boolean;
|
|
78
|
+
};
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
function MarkedProviderWithNativeParser(props: ParentProps) {
|
|
72
|
-
const platform = usePlatform()
|
|
73
|
-
return
|
|
83
|
+
const platform = usePlatform();
|
|
84
|
+
return (
|
|
85
|
+
<MarkedProvider nativeParser={platform.parseMarkdown}>
|
|
86
|
+
{props.children}
|
|
87
|
+
</MarkedProvider>
|
|
88
|
+
);
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
function AppShellProviders(props: ParentProps) {
|
|
@@ -90,7 +105,7 @@ function AppShellProviders(props: ParentProps) {
|
|
|
90
105
|
</LayoutProvider>
|
|
91
106
|
</PermissionProvider>
|
|
92
107
|
</SettingsProvider>
|
|
93
|
-
)
|
|
108
|
+
);
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
function SessionProviders(props: ParentProps) {
|
|
@@ -102,7 +117,7 @@ function SessionProviders(props: ParentProps) {
|
|
|
102
117
|
</PromptProvider>
|
|
103
118
|
</FileProvider>
|
|
104
119
|
</TerminalProvider>
|
|
105
|
-
)
|
|
120
|
+
);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
|
@@ -111,7 +126,7 @@ function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
|
|
|
111
126
|
{props.appChildren}
|
|
112
127
|
{props.children}
|
|
113
128
|
</AppShellProviders>
|
|
114
|
-
)
|
|
129
|
+
);
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
export function AppBaseProviders(props: ParentProps) {
|
|
@@ -124,7 +139,9 @@ export function AppBaseProviders(props: ParentProps) {
|
|
|
124
139
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
|
125
140
|
<DialogProvider>
|
|
126
141
|
<MarkedProviderWithNativeParser>
|
|
127
|
-
<FileComponentProvider component={File}>
|
|
142
|
+
<FileComponentProvider component={File}>
|
|
143
|
+
{props.children}
|
|
144
|
+
</FileComponentProvider>
|
|
128
145
|
</MarkedProviderWithNativeParser>
|
|
129
146
|
</DialogProvider>
|
|
130
147
|
</ErrorBoundary>
|
|
@@ -132,22 +149,22 @@ export function AppBaseProviders(props: ParentProps) {
|
|
|
132
149
|
</LanguageProvider>
|
|
133
150
|
</ThemeProvider>
|
|
134
151
|
</MetaProvider>
|
|
135
|
-
)
|
|
152
|
+
);
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
function ServerKey(props: ParentProps) {
|
|
139
|
-
const server = useServer()
|
|
156
|
+
const server = useServer();
|
|
140
157
|
return (
|
|
141
158
|
<Show when={server.key} keyed>
|
|
142
159
|
{props.children}
|
|
143
160
|
</Show>
|
|
144
|
-
)
|
|
161
|
+
);
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
export function AppInterface(props: {
|
|
148
|
-
children?: JSX.Element
|
|
149
|
-
defaultServer: ServerConnection.Key
|
|
150
|
-
servers?: Array<ServerConnection.Any
|
|
165
|
+
children?: JSX.Element;
|
|
166
|
+
defaultServer: ServerConnection.Key;
|
|
167
|
+
servers?: Array<ServerConnection.Any>;
|
|
151
168
|
}) {
|
|
152
169
|
return (
|
|
153
170
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
|
@@ -156,17 +173,21 @@ export function AppInterface(props: {
|
|
|
156
173
|
<GlobalSyncProvider>
|
|
157
174
|
<Router
|
|
158
175
|
base={(import.meta.env.BASE_URL ?? "/").replace(/\/+$/, "")}
|
|
159
|
-
root={(routerProps) =>
|
|
176
|
+
root={(routerProps) => (
|
|
177
|
+
<RouterRoot appChildren={props.children}>
|
|
178
|
+
{routerProps.children}
|
|
179
|
+
</RouterRoot>
|
|
180
|
+
)}
|
|
160
181
|
>
|
|
161
182
|
<Route path="/" component={HomeRoute} />
|
|
162
183
|
<Route path="/:dir" component={DirectoryLayout}>
|
|
163
184
|
<Route path="/" component={SessionIndexRoute} />
|
|
164
|
-
<Route path="
|
|
185
|
+
<Route path="/:id?" component={SessionRoute} />
|
|
165
186
|
</Route>
|
|
166
187
|
</Router>
|
|
167
188
|
</GlobalSyncProvider>
|
|
168
189
|
</GlobalSDKProvider>
|
|
169
190
|
</ServerKey>
|
|
170
191
|
</ServerProvider>
|
|
171
|
-
)
|
|
192
|
+
);
|
|
172
193
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useDialog } from "ikanban-ui/context/dialog"
|
|
2
|
+
import { Dialog } from "ikanban-ui/dialog"
|
|
3
|
+
import { Keybind } from "ikanban-ui/keybind"
|
|
4
|
+
import { List } from "ikanban-ui/list"
|
|
5
|
+
import { createMemo, onCleanup, Show } from "solid-js"
|
|
6
|
+
import { type CommandOption, formatKeybind, useCommand } from "@/context/command"
|
|
7
|
+
import { useLanguage } from "@/context/language"
|
|
8
|
+
|
|
9
|
+
export function DialogCommandPalette() {
|
|
10
|
+
const command = useCommand()
|
|
11
|
+
const dialog = useDialog()
|
|
12
|
+
const language = useLanguage()
|
|
13
|
+
const state = {
|
|
14
|
+
cleanup: undefined as (() => void) | void,
|
|
15
|
+
committed: false,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const items = createMemo(() =>
|
|
19
|
+
command.options.filter((option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const handleMove = (item: CommandOption | undefined) => {
|
|
23
|
+
state.cleanup?.()
|
|
24
|
+
state.cleanup = item?.onHighlight?.()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onCleanup(() => {
|
|
28
|
+
if (state.committed) return
|
|
29
|
+
state.cleanup?.()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
|
|
34
|
+
<List
|
|
35
|
+
search={{
|
|
36
|
+
placeholder: language.t("palette.search.placeholder"),
|
|
37
|
+
autofocus: true,
|
|
38
|
+
hideIcon: true,
|
|
39
|
+
}}
|
|
40
|
+
emptyMessage={language.t("palette.empty")}
|
|
41
|
+
items={items}
|
|
42
|
+
key={(item) => item.id}
|
|
43
|
+
filterKeys={["title", "description", "category"]}
|
|
44
|
+
groupBy={(item) => item.category ?? language.t("palette.group.commands")}
|
|
45
|
+
onMove={handleMove}
|
|
46
|
+
onSelect={(item) => {
|
|
47
|
+
if (!item) return
|
|
48
|
+
state.committed = true
|
|
49
|
+
state.cleanup = undefined
|
|
50
|
+
dialog.close()
|
|
51
|
+
item.onSelect?.("palette")
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{(item) => (
|
|
55
|
+
<div class="w-full flex items-center justify-between gap-4">
|
|
56
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
57
|
+
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
|
|
58
|
+
<Show when={item.description}>
|
|
59
|
+
<span class="text-14-regular text-text-weak truncate">{item.description}</span>
|
|
60
|
+
</Show>
|
|
61
|
+
</div>
|
|
62
|
+
<Show when={item.keybind}>
|
|
63
|
+
<Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
|
|
64
|
+
</Show>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</List>
|
|
68
|
+
</Dialog>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -75,7 +75,7 @@ export const DialogFork: Component = () => {
|
|
|
75
75
|
return
|
|
76
76
|
}
|
|
77
77
|
dialog.close()
|
|
78
|
-
navigate(`/${base64Encode(sdk.directory)}
|
|
78
|
+
navigate(`/${base64Encode(sdk.directory)}/${forked.data.id}`)
|
|
79
79
|
requestAnimationFrame(() => {
|
|
80
80
|
prompt.set(restored)
|
|
81
81
|
})
|
|
@@ -365,7 +365,7 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
|
|
|
365
365
|
|
|
366
366
|
if (item.type === "session") {
|
|
367
367
|
if (!item.directory || !item.sessionID) return
|
|
368
|
-
navigate(`/${base64Encode(item.directory)}
|
|
368
|
+
navigate(`/${base64Encode(item.directory)}/${item.sessionID}`)
|
|
369
369
|
return
|
|
370
370
|
}
|
|
371
371
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
const dialogSource = readFileSync(join(import.meta.dir, "dialog-select-server.tsx"), "utf8")
|
|
6
|
+
const frameSource = readFileSync(join(import.meta.dir, "server-list-item-frame.tsx"), "utf8")
|
|
7
|
+
|
|
8
|
+
describe("DialogSelectServer structure", () => {
|
|
9
|
+
test("wraps each http server row so menu actions stay outside the list button", () => {
|
|
10
|
+
expect(dialogSource).toContain("itemWrapper={(item, node) =>")
|
|
11
|
+
expect(dialogSource).toContain("<ServerListItemFrame")
|
|
12
|
+
expect(dialogSource).toContain("row={node}")
|
|
13
|
+
expect(dialogSource).toContain("actions={")
|
|
14
|
+
expect(frameSource).toContain('data-slot="server-list-item-actions"')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -11,6 +11,7 @@ import { useNavigate } from "@solidjs/router"
|
|
|
11
11
|
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
|
|
12
12
|
import { createStore, reconcile } from "solid-js/store"
|
|
13
13
|
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
|
|
14
|
+
import { ServerListItemFrame } from "@/components/server-list-item-frame"
|
|
14
15
|
import { useLanguage } from "@/context/language"
|
|
15
16
|
import { usePlatform } from "@/context/platform"
|
|
16
17
|
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
|
|
@@ -532,6 +533,61 @@ export function DialogSelectServer() {
|
|
|
532
533
|
emptyMessage={language.t("dialog.server.empty")}
|
|
533
534
|
items={sortedItems}
|
|
534
535
|
key={(x) => x.http.url}
|
|
536
|
+
itemWrapper={(item, node) => (
|
|
537
|
+
<Show
|
|
538
|
+
when={item.type === "http"}
|
|
539
|
+
fallback={node}
|
|
540
|
+
>
|
|
541
|
+
<ServerListItemFrame
|
|
542
|
+
row={node}
|
|
543
|
+
actions={
|
|
544
|
+
<DropdownMenu>
|
|
545
|
+
<DropdownMenu.Trigger
|
|
546
|
+
as={IconButton}
|
|
547
|
+
icon="dot-grid"
|
|
548
|
+
variant="ghost"
|
|
549
|
+
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
|
550
|
+
onClick={(e: MouseEvent) => e.stopPropagation()}
|
|
551
|
+
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
|
552
|
+
/>
|
|
553
|
+
<DropdownMenu.Portal>
|
|
554
|
+
<DropdownMenu.Content class="mt-1">
|
|
555
|
+
<DropdownMenu.Item
|
|
556
|
+
onSelect={() => {
|
|
557
|
+
if (item.type !== "http") return
|
|
558
|
+
startEdit(item)
|
|
559
|
+
}}
|
|
560
|
+
>
|
|
561
|
+
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
|
562
|
+
</DropdownMenu.Item>
|
|
563
|
+
<Show when={canDefault() && defaultUrl() !== item.http.url}>
|
|
564
|
+
<DropdownMenu.Item onSelect={() => setDefault(item.http.url)}>
|
|
565
|
+
<DropdownMenu.ItemLabel>
|
|
566
|
+
{language.t("dialog.server.menu.default")}
|
|
567
|
+
</DropdownMenu.ItemLabel>
|
|
568
|
+
</DropdownMenu.Item>
|
|
569
|
+
</Show>
|
|
570
|
+
<Show when={canDefault() && defaultUrl() === item.http.url}>
|
|
571
|
+
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
|
572
|
+
<DropdownMenu.ItemLabel>
|
|
573
|
+
{language.t("dialog.server.menu.defaultRemove")}
|
|
574
|
+
</DropdownMenu.ItemLabel>
|
|
575
|
+
</DropdownMenu.Item>
|
|
576
|
+
</Show>
|
|
577
|
+
<DropdownMenu.Separator />
|
|
578
|
+
<DropdownMenu.Item
|
|
579
|
+
onSelect={() => handleRemove(ServerConnection.key(item))}
|
|
580
|
+
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
|
581
|
+
>
|
|
582
|
+
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
|
583
|
+
</DropdownMenu.Item>
|
|
584
|
+
</DropdownMenu.Content>
|
|
585
|
+
</DropdownMenu.Portal>
|
|
586
|
+
</DropdownMenu>
|
|
587
|
+
}
|
|
588
|
+
/>
|
|
589
|
+
</Show>
|
|
590
|
+
)}
|
|
535
591
|
onSelect={(x) => {
|
|
536
592
|
if (x) select(x)
|
|
537
593
|
}}
|
|
@@ -559,57 +615,9 @@ export function DialogSelectServer() {
|
|
|
559
615
|
}
|
|
560
616
|
showCredentials
|
|
561
617
|
/>
|
|
562
|
-
<
|
|
563
|
-
<
|
|
564
|
-
|
|
565
|
-
</Show>
|
|
566
|
-
|
|
567
|
-
<Show when={i.type === "http"}>
|
|
568
|
-
<DropdownMenu>
|
|
569
|
-
<DropdownMenu.Trigger
|
|
570
|
-
as={IconButton}
|
|
571
|
-
icon="dot-grid"
|
|
572
|
-
variant="ghost"
|
|
573
|
-
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
|
574
|
-
onClick={(e: MouseEvent) => e.stopPropagation()}
|
|
575
|
-
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
|
576
|
-
/>
|
|
577
|
-
<DropdownMenu.Portal>
|
|
578
|
-
<DropdownMenu.Content class="mt-1">
|
|
579
|
-
<DropdownMenu.Item
|
|
580
|
-
onSelect={() => {
|
|
581
|
-
if (i.type !== "http") return
|
|
582
|
-
startEdit(i)
|
|
583
|
-
}}
|
|
584
|
-
>
|
|
585
|
-
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
|
586
|
-
</DropdownMenu.Item>
|
|
587
|
-
<Show when={canDefault() && defaultUrl() !== i.http.url}>
|
|
588
|
-
<DropdownMenu.Item onSelect={() => setDefault(i.http.url)}>
|
|
589
|
-
<DropdownMenu.ItemLabel>
|
|
590
|
-
{language.t("dialog.server.menu.default")}
|
|
591
|
-
</DropdownMenu.ItemLabel>
|
|
592
|
-
</DropdownMenu.Item>
|
|
593
|
-
</Show>
|
|
594
|
-
<Show when={canDefault() && defaultUrl() === i.http.url}>
|
|
595
|
-
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
|
596
|
-
<DropdownMenu.ItemLabel>
|
|
597
|
-
{language.t("dialog.server.menu.defaultRemove")}
|
|
598
|
-
</DropdownMenu.ItemLabel>
|
|
599
|
-
</DropdownMenu.Item>
|
|
600
|
-
</Show>
|
|
601
|
-
<DropdownMenu.Separator />
|
|
602
|
-
<DropdownMenu.Item
|
|
603
|
-
onSelect={() => handleRemove(ServerConnection.key(i))}
|
|
604
|
-
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
|
605
|
-
>
|
|
606
|
-
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
|
607
|
-
</DropdownMenu.Item>
|
|
608
|
-
</DropdownMenu.Content>
|
|
609
|
-
</DropdownMenu.Portal>
|
|
610
|
-
</DropdownMenu>
|
|
611
|
-
</Show>
|
|
612
|
-
</div>
|
|
618
|
+
<Show when={ServerConnection.key(current()) === key}>
|
|
619
|
+
<Icon name="check" class="h-6" />
|
|
620
|
+
</Show>
|
|
613
621
|
</div>
|
|
614
622
|
)
|
|
615
623
|
}}
|
|
@@ -203,7 +203,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|
|
203
203
|
if (session) {
|
|
204
204
|
if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory)
|
|
205
205
|
layout.handoff.setTabs(base64Encode(sessionDirectory), session.id)
|
|
206
|
-
navigate(`/${base64Encode(sessionDirectory)}
|
|
206
|
+
navigate(`/${base64Encode(sessionDirectory)}/${session.id}`)
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
if (!session) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** @jsxImportSource solid-js */
|
|
2
|
+
import { Show, type JSX } from "solid-js"
|
|
3
|
+
|
|
4
|
+
export function ServerListItemFrame(props: { row: JSX.Element; actions?: JSX.Element }) {
|
|
5
|
+
return (
|
|
6
|
+
<div data-slot="server-list-item-frame" class="flex min-w-0 w-full items-center gap-1">
|
|
7
|
+
<div class="min-w-0 flex-1">{props.row}</div>
|
|
8
|
+
<Show when={props.actions}>
|
|
9
|
+
<div data-slot="server-list-item-actions" class="flex shrink-0 items-center justify-center pr-3">
|
|
10
|
+
{props.actions}
|
|
11
|
+
</div>
|
|
12
|
+
</Show>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -2,7 +2,6 @@ import { createEffect, createMemo, Show, untrack } from "solid-js"
|
|
|
2
2
|
import { createStore } from "solid-js/store"
|
|
3
3
|
import { useLocation, useNavigate, useParams } from "@solidjs/router"
|
|
4
4
|
import { IconButton } from "ikanban-ui/icon-button"
|
|
5
|
-
import { Icon } from "ikanban-ui/icon"
|
|
6
5
|
import { Button } from "ikanban-ui/button"
|
|
7
6
|
import { Tooltip, TooltipKeybind } from "ikanban-ui/tooltip"
|
|
8
7
|
import { useTheme } from "ikanban-ui/theme"
|
|
@@ -47,7 +46,6 @@ export function Titlebar() {
|
|
|
47
46
|
|
|
48
47
|
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
|
49
48
|
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
|
|
50
|
-
const web = createMemo(() => platform.platform === "web")
|
|
51
49
|
const zoom = () => platform.webviewZoom?.() ?? 1
|
|
52
50
|
const minHeight = () => (mac() ? `${40 / zoom()}px` : undefined)
|
|
53
51
|
|
|
@@ -192,34 +190,17 @@ export function Titlebar() {
|
|
|
192
190
|
</div>
|
|
193
191
|
</Show>
|
|
194
192
|
<div class="flex items-center gap-1 shrink-0">
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
aria-expanded={layout.sidebar.opened()}
|
|
207
|
-
>
|
|
208
|
-
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
|
209
|
-
<Icon
|
|
210
|
-
size="small"
|
|
211
|
-
name={layout.sidebar.opened() ? "layout-left-partial" : "layout-left"}
|
|
212
|
-
class="group-hover/sidebar-toggle:hidden"
|
|
213
|
-
/>
|
|
214
|
-
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
|
|
215
|
-
<Icon
|
|
216
|
-
size="small"
|
|
217
|
-
name={layout.sidebar.opened() ? "layout-left" : "layout-left-partial"}
|
|
218
|
-
class="hidden group-active/sidebar-toggle:inline-block"
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
</Button>
|
|
222
|
-
</TooltipKeybind>
|
|
193
|
+
<Show when={params.dir}>
|
|
194
|
+
<Tooltip placement="bottom" value={language.t("home.title")} openDelay={2000}>
|
|
195
|
+
<Button
|
|
196
|
+
variant="ghost"
|
|
197
|
+
icon="arrow-left"
|
|
198
|
+
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
199
|
+
onClick={() => navigate("/")}
|
|
200
|
+
aria-label={language.t("home.title")}
|
|
201
|
+
/>
|
|
202
|
+
</Tooltip>
|
|
203
|
+
</Show>
|
|
223
204
|
<div class="hidden xl:flex items-center shrink-0">
|
|
224
205
|
<Show when={params.dir}>
|
|
225
206
|
<TooltipKeybind
|
|
@@ -234,7 +215,7 @@ export function Titlebar() {
|
|
|
234
215
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
235
216
|
onClick={() => {
|
|
236
217
|
if (!params.dir) return
|
|
237
|
-
navigate(`/${params.dir}
|
|
218
|
+
navigate(`/${params.dir}`)
|
|
238
219
|
}}
|
|
239
220
|
aria-label={language.t("command.session.new")}
|
|
240
221
|
/>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function normalizeBase(base: string) {
|
|
2
|
+
const trimmed = base.replace(/\/+$/, "")
|
|
3
|
+
if (!trimmed) return "/"
|
|
4
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function stripBase(pathname: string, base: string) {
|
|
8
|
+
const normalizedBase = normalizeBase(base)
|
|
9
|
+
if (normalizedBase === "/") return pathname || "/"
|
|
10
|
+
if (pathname === normalizedBase) return "/"
|
|
11
|
+
if (pathname.startsWith(`${normalizedBase}/`)) return pathname.slice(normalizedBase.length) || "/"
|
|
12
|
+
return pathname || "/"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function shouldLoadProjectsOnBootstrap(pathname: string, base: string) {
|
|
16
|
+
return stripBase(pathname, base) !== "/"
|
|
17
|
+
}
|
|
@@ -39,6 +39,7 @@ export async function bootstrapGlobal(input: {
|
|
|
39
39
|
unknownError: string
|
|
40
40
|
invalidConfigurationError: string
|
|
41
41
|
formatMoreCount: (count: number) => string
|
|
42
|
+
loadProjects?: boolean
|
|
42
43
|
setGlobalStore: SetStoreFunction<GlobalStore>
|
|
43
44
|
}) {
|
|
44
45
|
const health = await input.globalSDK.global
|
|
@@ -66,16 +67,6 @@ export async function bootstrapGlobal(input: {
|
|
|
66
67
|
input.setGlobalStore("config", x.data!)
|
|
67
68
|
}),
|
|
68
69
|
),
|
|
69
|
-
retry(() =>
|
|
70
|
-
input.globalSDK.project.list().then((x) => {
|
|
71
|
-
const projects = (x.data ?? [])
|
|
72
|
-
.filter((p) => !!p?.id)
|
|
73
|
-
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
|
74
|
-
.slice()
|
|
75
|
-
.sort((a, b) => cmp(a.id, b.id))
|
|
76
|
-
input.setGlobalStore("project", projects)
|
|
77
|
-
}),
|
|
78
|
-
),
|
|
79
70
|
retry(() =>
|
|
80
71
|
input.globalSDK.provider.list().then((x) => {
|
|
81
72
|
input.setGlobalStore("provider", normalizeProviderList(x.data!))
|
|
@@ -88,6 +79,23 @@ export async function bootstrapGlobal(input: {
|
|
|
88
79
|
),
|
|
89
80
|
]
|
|
90
81
|
|
|
82
|
+
if (input.loadProjects !== false) {
|
|
83
|
+
tasks.splice(
|
|
84
|
+
2,
|
|
85
|
+
0,
|
|
86
|
+
retry(() =>
|
|
87
|
+
input.globalSDK.project.list().then((x) => {
|
|
88
|
+
const projects = (x.data ?? [])
|
|
89
|
+
.filter((p) => !!p?.id)
|
|
90
|
+
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
|
|
91
|
+
.slice()
|
|
92
|
+
.sort((a, b) => cmp(a.id, b.id))
|
|
93
|
+
input.setGlobalStore("project", projects)
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
const results = await Promise.allSettled(tasks)
|
|
92
100
|
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
|
|
93
101
|
if (errors.length) {
|