ikanban-web 0.2.10 → 0.2.11
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-BOsxUEnx.js} +1 -1
- package/dist/assets/home-DmwjxlRp.js +1 -0
- package/dist/assets/{index-n2BMSLCz.js → index-CE2ZqGRp.js} +107 -107
- package/dist/assets/{index-DbuHW0gF.css → index-Z4u1EVXA.css} +1 -1
- package/dist/assets/session-FqpMK7PV.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/prompt-input/submit.ts +1 -1
- package/src/components/titlebar.tsx +12 -1
- package/src/context/notification.tsx +2 -2
- 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-workspace.tsx +1 -1
- package/src/pages/layout.tsx +174 -173
- 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-CE2ZqGRp.js"></script>
|
|
31
|
+
<link rel="stylesheet" crossorigin href="/ikanban/assets/index-Z4u1EVXA.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
|
|
|
@@ -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) {
|
|
@@ -192,6 +192,17 @@ export function Titlebar() {
|
|
|
192
192
|
</div>
|
|
193
193
|
</Show>
|
|
194
194
|
<div class="flex items-center gap-1 shrink-0">
|
|
195
|
+
<Show when={params.dir}>
|
|
196
|
+
<Tooltip placement="bottom" value={language.t("home.title")} openDelay={2000}>
|
|
197
|
+
<Button
|
|
198
|
+
variant="ghost"
|
|
199
|
+
icon="arrow-left"
|
|
200
|
+
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
201
|
+
onClick={() => navigate("/")}
|
|
202
|
+
aria-label={language.t("home.title")}
|
|
203
|
+
/>
|
|
204
|
+
</Tooltip>
|
|
205
|
+
</Show>
|
|
195
206
|
<TooltipKeybind
|
|
196
207
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
|
|
197
208
|
placement="bottom"
|
|
@@ -234,7 +245,7 @@ export function Titlebar() {
|
|
|
234
245
|
class="titlebar-icon w-8 h-6 p-0 box-border"
|
|
235
246
|
onClick={() => {
|
|
236
247
|
if (!params.dir) return
|
|
237
|
-
navigate(`/${params.dir}
|
|
248
|
+
navigate(`/${params.dir}`)
|
|
238
249
|
}}
|
|
239
250
|
aria-label={language.t("command.session.new")}
|
|
240
251
|
/>
|
|
@@ -245,7 +245,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|
|
245
245
|
session: sessionID,
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
-
const href = `/${base64Encode(directory)}
|
|
248
|
+
const href = `/${base64Encode(directory)}/${sessionID}`
|
|
249
249
|
if (settings.notifications.agent()) {
|
|
250
250
|
void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href)
|
|
251
251
|
}
|
|
@@ -278,7 +278,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|
|
278
278
|
const description =
|
|
279
279
|
session?.title ??
|
|
280
280
|
(typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
|
|
281
|
-
const href = sessionID ? `/${base64Encode(directory)}
|
|
281
|
+
const href = sessionID ? `/${base64Encode(directory)}/${sessionID}` : `/${base64Encode(directory)}`
|
|
282
282
|
if (settings.notifications.errors()) {
|
|
283
283
|
void platform.notify(language.t("notification.session.error.title"), description, href)
|
|
284
284
|
}
|
|
@@ -19,8 +19,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
|
|
|
19
19
|
<DataProvider
|
|
20
20
|
data={sync.data}
|
|
21
21
|
directory={props.directory}
|
|
22
|
-
onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}
|
|
23
|
-
onSessionHref={(sessionID: string) => `/${params.dir}
|
|
22
|
+
onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/${sessionID}`)}
|
|
23
|
+
onSessionHref={(sessionID: string) => `/${params.dir}/${sessionID}`}
|
|
24
24
|
>
|
|
25
25
|
<LocalProvider>{props.children}</LocalProvider>
|
|
26
26
|
</DataProvider>
|
package/src/pages/home.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createMemo, createEffect, For, Match, Switch } from "solid-js";
|
|
1
|
+
import { createMemo, createEffect, createResource, For, Match, Switch } from "solid-js";
|
|
2
2
|
import { Button } from "ikanban-ui/button";
|
|
3
3
|
import { Logo } from "ikanban-ui/logo";
|
|
4
4
|
import { useLayout } from "@/context/layout";
|
|
@@ -15,7 +15,6 @@ import { useServer } from "@/context/server";
|
|
|
15
15
|
import { useGlobalSync } from "@/context/global-sync";
|
|
16
16
|
import { useLanguage } from "@/context/language";
|
|
17
17
|
import { IconButton } from "ikanban-ui/icon-button";
|
|
18
|
-
import { sortedRootSessions } from "@/pages/layout/helpers";
|
|
19
18
|
import type { Session } from "@opencode-ai/sdk/v2/client";
|
|
20
19
|
|
|
21
20
|
type BoardColumn = "progress" | "idle";
|
|
@@ -26,6 +25,34 @@ type BoardCard = {
|
|
|
26
25
|
updatedAt: number;
|
|
27
26
|
};
|
|
28
27
|
|
|
28
|
+
const homeStyles = {
|
|
29
|
+
border: { border: "1px solid var(--border-weak-base)" },
|
|
30
|
+
heroIcon: {
|
|
31
|
+
border: "1px solid var(--border-weak-base)",
|
|
32
|
+
background:
|
|
33
|
+
"linear-gradient(135deg, color-mix(in srgb, var(--text-interactive-base) 12%, var(--background-stronger)), var(--background-stronger))",
|
|
34
|
+
},
|
|
35
|
+
emptyPanel: {
|
|
36
|
+
"border-color": "var(--border-weak-base)",
|
|
37
|
+
background:
|
|
38
|
+
"linear-gradient(180deg, color-mix(in srgb, var(--surface-base-hover) 35%, var(--background-stronger)), var(--background-stronger))",
|
|
39
|
+
},
|
|
40
|
+
emptyIcon: {
|
|
41
|
+
border: "1px solid var(--border-weak-base)",
|
|
42
|
+
background:
|
|
43
|
+
"linear-gradient(135deg, color-mix(in srgb, var(--text-interactive-base) 10%, var(--surface-inset-base)), var(--surface-inset-base))",
|
|
44
|
+
},
|
|
45
|
+
card: {
|
|
46
|
+
border: "1px solid var(--border-weak-base)",
|
|
47
|
+
background: "var(--background-base)",
|
|
48
|
+
},
|
|
49
|
+
emptyState: {
|
|
50
|
+
"border-color": "var(--border-weak-base)",
|
|
51
|
+
background:
|
|
52
|
+
"linear-gradient(180deg, color-mix(in srgb, var(--surface-base-hover) 30%, var(--surface-inset-base)), var(--surface-inset-base))",
|
|
53
|
+
},
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
29
56
|
export default function Home() {
|
|
30
57
|
const sync = useGlobalSync();
|
|
31
58
|
const layout = useLayout();
|
|
@@ -35,26 +62,49 @@ export default function Home() {
|
|
|
35
62
|
const globalSDK = useGlobalSDK();
|
|
36
63
|
const server = useServer();
|
|
37
64
|
const language = useLanguage();
|
|
38
|
-
const
|
|
65
|
+
const projects = createMemo(() => {
|
|
39
66
|
return sync.data.project
|
|
40
67
|
.slice()
|
|
41
68
|
.sort(
|
|
42
69
|
(a, b) =>
|
|
43
70
|
(b.time.updated ?? b.time.created) -
|
|
44
71
|
(a.time.updated ?? a.time.created),
|
|
45
|
-
)
|
|
46
|
-
.slice(0, 5);
|
|
72
|
+
);
|
|
47
73
|
});
|
|
74
|
+
|
|
75
|
+
const [rootSessions, { mutate: mutateRootSessions }] = createResource(
|
|
76
|
+
() => projects().map((project) => project.worktree),
|
|
77
|
+
async (directories) => {
|
|
78
|
+
const entries = await Promise.all(
|
|
79
|
+
directories.map(async (directory) => {
|
|
80
|
+
const result = await globalSDK.client.session.list({
|
|
81
|
+
directory,
|
|
82
|
+
roots: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const sessions = (result.data ?? []).filter(
|
|
86
|
+
(session): session is Session => !!session?.id && !session.time?.archived,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return [directory, sessions] as const;
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return Object.fromEntries(entries) as Record<string, Session[]>;
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
48
97
|
const boardColumns = createMemo<Record<BoardColumn, BoardCard[]>>(() => {
|
|
49
98
|
const now = Date.now();
|
|
50
99
|
const columns: Record<BoardColumn, BoardCard[]> = {
|
|
51
100
|
progress: [],
|
|
52
101
|
idle: [],
|
|
53
102
|
};
|
|
103
|
+
const sessionsByProject = rootSessions() ?? {};
|
|
54
104
|
|
|
55
|
-
for (const project of
|
|
105
|
+
for (const project of projects()) {
|
|
56
106
|
const [store] = sync.child(project.worktree);
|
|
57
|
-
const sessions =
|
|
107
|
+
const sessions = sessionsByProject[project.worktree] ?? [];
|
|
58
108
|
|
|
59
109
|
for (const session of sessions) {
|
|
60
110
|
const status = store.session_status[session.id];
|
|
@@ -82,7 +132,7 @@ export default function Home() {
|
|
|
82
132
|
});
|
|
83
133
|
|
|
84
134
|
createEffect(() => {
|
|
85
|
-
for (const project of
|
|
135
|
+
for (const project of projects()) {
|
|
86
136
|
sync.child(project.worktree);
|
|
87
137
|
}
|
|
88
138
|
});
|
|
@@ -97,13 +147,13 @@ export default function Home() {
|
|
|
97
147
|
function openProject(directory: string) {
|
|
98
148
|
layout.projects.open(directory);
|
|
99
149
|
server.projects.touch(directory);
|
|
100
|
-
navigate(`/${base64Encode(directory)}
|
|
150
|
+
navigate(`/${base64Encode(directory)}`);
|
|
101
151
|
}
|
|
102
152
|
|
|
103
153
|
function openSession(directory: string, sessionID: string) {
|
|
104
154
|
layout.projects.open(directory);
|
|
105
155
|
server.projects.touch(directory);
|
|
106
|
-
navigate(`/${base64Encode(directory)}
|
|
156
|
+
navigate(`/${base64Encode(directory)}/${sessionID}`);
|
|
107
157
|
}
|
|
108
158
|
|
|
109
159
|
async function archiveSession(directory: string, sessionID: string) {
|
|
@@ -112,6 +162,13 @@ export default function Home() {
|
|
|
112
162
|
sessionID,
|
|
113
163
|
time: { archived: Date.now() },
|
|
114
164
|
});
|
|
165
|
+
mutateRootSessions((current) => {
|
|
166
|
+
if (!current?.[directory]) return current;
|
|
167
|
+
return {
|
|
168
|
+
...current,
|
|
169
|
+
[directory]: current[directory].filter((session) => session.id !== sessionID),
|
|
170
|
+
};
|
|
171
|
+
});
|
|
115
172
|
setStore(
|
|
116
173
|
"session",
|
|
117
174
|
(sessions) => sessions.filter((session) => session.id !== sessionID),
|
|
@@ -148,7 +205,10 @@ export default function Home() {
|
|
|
148
205
|
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
149
206
|
<div class="min-w-0">
|
|
150
207
|
<div class="flex items-center gap-3">
|
|
151
|
-
<div
|
|
208
|
+
<div
|
|
209
|
+
class="flex size-10 items-center justify-center rounded-2xl border shadow-xs-border-base"
|
|
210
|
+
style={homeStyles.heroIcon}
|
|
211
|
+
>
|
|
152
212
|
<Logo class="w-6 opacity-90" />
|
|
153
213
|
</div>
|
|
154
214
|
<div>
|
|
@@ -189,7 +249,7 @@ export default function Home() {
|
|
|
189
249
|
</div>
|
|
190
250
|
|
|
191
251
|
<Switch>
|
|
192
|
-
<Match when={
|
|
252
|
+
<Match when={projects().length > 0}>
|
|
193
253
|
<div class="mt-6 flex w-full flex-col gap-6">
|
|
194
254
|
<section class="flex flex-col gap-4">
|
|
195
255
|
<div class="grid gap-3 lg:grid-cols-2">
|
|
@@ -216,9 +276,15 @@ export default function Home() {
|
|
|
216
276
|
</div>
|
|
217
277
|
</Match>
|
|
218
278
|
<Match when={true}>
|
|
219
|
-
<div
|
|
279
|
+
<div
|
|
280
|
+
class="mt-6 rounded-2xl border border-dashed px-6 py-10 shadow-xs-border-base"
|
|
281
|
+
style={homeStyles.emptyPanel}
|
|
282
|
+
>
|
|
220
283
|
<div class="mx-auto flex max-w-md flex-col items-center gap-3 text-center">
|
|
221
|
-
<div
|
|
284
|
+
<div
|
|
285
|
+
class="flex size-12 items-center justify-center rounded-2xl border shadow-xs-border-base"
|
|
286
|
+
style={homeStyles.emptyIcon}
|
|
287
|
+
>
|
|
222
288
|
<Icon name="folder-add-left" size="large" />
|
|
223
289
|
</div>
|
|
224
290
|
<div class="flex flex-col gap-1 items-center justify-center">
|
|
@@ -251,12 +317,25 @@ function BoardColumnView(props: {
|
|
|
251
317
|
}) {
|
|
252
318
|
const toneClass = () => {
|
|
253
319
|
if (props.tone === "progress")
|
|
254
|
-
return "bg-
|
|
255
|
-
return "bg-
|
|
320
|
+
return "bg-surface-base-hover text-icon-success-base border-border-weak-base";
|
|
321
|
+
return "bg-surface-inset-base text-icon-base border-border-weak-base";
|
|
256
322
|
};
|
|
257
323
|
|
|
324
|
+
const sectionStyle = () =>
|
|
325
|
+
props.tone === "progress"
|
|
326
|
+
? {
|
|
327
|
+
...homeStyles.border,
|
|
328
|
+
background:
|
|
329
|
+
"linear-gradient(180deg, color-mix(in srgb, var(--text-interactive-base) 8%, var(--background-stronger)), var(--background-stronger))",
|
|
330
|
+
}
|
|
331
|
+
: {
|
|
332
|
+
...homeStyles.border,
|
|
333
|
+
background:
|
|
334
|
+
"linear-gradient(180deg, color-mix(in srgb, var(--surface-inset-base) 55%, var(--background-stronger)), var(--background-stronger))",
|
|
335
|
+
};
|
|
336
|
+
|
|
258
337
|
return (
|
|
259
|
-
<section class="rounded-2xl
|
|
338
|
+
<section class="rounded-2xl p-3 md:p-4 min-h-72 shadow-xs-border-base" style={sectionStyle()}>
|
|
260
339
|
<div class="flex items-center justify-between gap-3">
|
|
261
340
|
<div class="flex items-center gap-2 min-w-0">
|
|
262
341
|
<div
|
|
@@ -274,7 +353,10 @@ function BoardColumnView(props: {
|
|
|
274
353
|
<Match when={props.cards.length > 0}>
|
|
275
354
|
<For each={props.cards}>
|
|
276
355
|
{(card) => (
|
|
277
|
-
<div
|
|
356
|
+
<div
|
|
357
|
+
class="overflow-hidden rounded-xl transition-colors"
|
|
358
|
+
style={homeStyles.card}
|
|
359
|
+
>
|
|
278
360
|
<div class="flex items-start justify-between gap-3 px-3 pt-3">
|
|
279
361
|
<div class="min-w-0 flex-1 pt-1">
|
|
280
362
|
<div class="truncate text-14-medium leading-tight text-text-strong">
|
|
@@ -320,7 +402,10 @@ function BoardColumnView(props: {
|
|
|
320
402
|
</For>
|
|
321
403
|
</Match>
|
|
322
404
|
<Match when={true}>
|
|
323
|
-
<div
|
|
405
|
+
<div
|
|
406
|
+
class="rounded-xl border border-dashed px-3 py-8 text-center text-12-regular text-text-weak"
|
|
407
|
+
style={homeStyles.emptyState}
|
|
408
|
+
>
|
|
324
409
|
{props.empty}
|
|
325
410
|
</div>
|
|
326
411
|
</Match>
|