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
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
loadRootSessionsWithFallback,
|
|
6
6
|
pickDirectoriesToEvict,
|
|
7
7
|
} from "./global-sync"
|
|
8
|
+
import { bootstrapGlobal } from "./global-sync/bootstrap"
|
|
9
|
+
import { shouldLoadProjectsOnBootstrap } from "./global-sync/bootstrap-mode"
|
|
8
10
|
|
|
9
11
|
describe("pickDirectoriesToEvict", () => {
|
|
10
12
|
test("keeps pinned stores and evicts idle stores", () => {
|
|
@@ -124,3 +126,58 @@ describe("canDisposeDirectory", () => {
|
|
|
124
126
|
).toBe(true)
|
|
125
127
|
})
|
|
126
128
|
})
|
|
129
|
+
|
|
130
|
+
describe("bootstrapGlobal", () => {
|
|
131
|
+
test("does not call project list when project loading is disabled", async () => {
|
|
132
|
+
let projectListCalls = 0
|
|
133
|
+
const project = {
|
|
134
|
+
list: async () => {
|
|
135
|
+
projectListCalls += 1
|
|
136
|
+
return { data: [] }
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const updates: Array<[string, unknown]> = []
|
|
141
|
+
|
|
142
|
+
await bootstrapGlobal({
|
|
143
|
+
globalSDK: {
|
|
144
|
+
global: {
|
|
145
|
+
health: async () => ({ data: { healthy: true } }),
|
|
146
|
+
config: { get: async () => ({ data: {} }) },
|
|
147
|
+
},
|
|
148
|
+
path: { get: async () => ({ data: { state: "", config: "", worktree: "", directory: "", home: "" } }) },
|
|
149
|
+
project,
|
|
150
|
+
provider: {
|
|
151
|
+
list: async () => ({ data: { all: [], connected: [], default: {} } }),
|
|
152
|
+
auth: async () => ({ data: {} }),
|
|
153
|
+
},
|
|
154
|
+
} as never,
|
|
155
|
+
connectErrorTitle: "connect",
|
|
156
|
+
connectErrorDescription: "connect desc",
|
|
157
|
+
requestFailedTitle: "request",
|
|
158
|
+
unknownError: "unknown",
|
|
159
|
+
invalidConfigurationError: "invalid",
|
|
160
|
+
formatMoreCount: () => "",
|
|
161
|
+
setGlobalStore: ((key: string, value: unknown) => {
|
|
162
|
+
updates.push([key, value])
|
|
163
|
+
return value
|
|
164
|
+
}) as never,
|
|
165
|
+
loadProjects: false,
|
|
166
|
+
} as never)
|
|
167
|
+
|
|
168
|
+
expect(projectListCalls).toBe(0)
|
|
169
|
+
expect(updates.some(([key]) => key === "ready")).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe("shouldLoadProjectsOnBootstrap", () => {
|
|
174
|
+
test("skips project loading on the home route", () => {
|
|
175
|
+
expect(shouldLoadProjectsOnBootstrap("/", "/")).toBe(false)
|
|
176
|
+
expect(shouldLoadProjectsOnBootstrap("/ikanban", "/ikanban/")).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test("loads projects away from the home route", () => {
|
|
180
|
+
expect(shouldLoadProjectsOnBootstrap("/dGVzdA", "/")).toBe(true)
|
|
181
|
+
expect(shouldLoadProjectsOnBootstrap("/ikanban/dGVzdA", "/ikanban/")).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -26,6 +26,7 @@ import { Persist, persisted } from "@/utils/persist"
|
|
|
26
26
|
import type { InitError } from "../pages/error"
|
|
27
27
|
import { useGlobalSDK } from "./global-sdk"
|
|
28
28
|
import { bootstrapDirectory, bootstrapGlobal } from "./global-sync/bootstrap"
|
|
29
|
+
import { shouldLoadProjectsOnBootstrap } from "./global-sync/bootstrap-mode"
|
|
29
30
|
import { createChildStoreManager } from "./global-sync/child-store"
|
|
30
31
|
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
|
|
31
32
|
import { createRefreshQueue } from "./global-sync/queue"
|
|
@@ -334,6 +335,10 @@ function createGlobalSync() {
|
|
|
334
335
|
unknownError: language.t("error.chain.unknown"),
|
|
335
336
|
invalidConfigurationError: language.t("error.server.invalidConfiguration"),
|
|
336
337
|
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
|
|
338
|
+
loadProjects: shouldLoadProjectsOnBootstrap(
|
|
339
|
+
globalThis.location?.pathname ?? "/",
|
|
340
|
+
import.meta.env.BASE_URL ?? "/",
|
|
341
|
+
),
|
|
337
342
|
setGlobalStore: setBootStore,
|
|
338
343
|
})
|
|
339
344
|
}
|
|
@@ -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
|
}
|
package/src/i18n/en.ts
CHANGED
|
@@ -133,7 +133,7 @@ export const dict = {
|
|
|
133
133
|
"Customize which models appear in the model selector.",
|
|
134
134
|
"dialog.model.manage.provider.toggle": "Toggle all {{provider}} models",
|
|
135
135
|
|
|
136
|
-
"dialog.model.unpaid.freeModels.title": "Free models provided by
|
|
136
|
+
"dialog.model.unpaid.freeModels.title": "Free models provided by iKanban",
|
|
137
137
|
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
|
|
138
138
|
|
|
139
139
|
"dialog.provider.viewAll": "Show more providers",
|
|
@@ -146,7 +146,7 @@ export const dict = {
|
|
|
146
146
|
"provider.connect.status.waiting": "Waiting for authorization...",
|
|
147
147
|
"provider.connect.status.failed": "Authorization failed: {{error}}",
|
|
148
148
|
"provider.connect.apiKey.description":
|
|
149
|
-
"Enter your {{provider}} API key to connect your account and use {{provider}} models in
|
|
149
|
+
"Enter your {{provider}} API key to connect your account and use {{provider}} models in iKanban.",
|
|
150
150
|
"provider.connect.apiKey.label": "{{provider}} API key",
|
|
151
151
|
"provider.connect.apiKey.placeholder": "API key",
|
|
152
152
|
"provider.connect.apiKey.required": "API key is required",
|
|
@@ -160,7 +160,7 @@ export const dict = {
|
|
|
160
160
|
"provider.connect.oauth.code.visit.prefix": "Visit ",
|
|
161
161
|
"provider.connect.oauth.code.visit.link": "this link",
|
|
162
162
|
"provider.connect.oauth.code.visit.suffix":
|
|
163
|
-
" to collect your authorization code to connect your account and use {{provider}} models in
|
|
163
|
+
" to collect your authorization code to connect your account and use {{provider}} models in iKanban.",
|
|
164
164
|
"provider.connect.oauth.code.label": "{{method}} authorization code",
|
|
165
165
|
"provider.connect.oauth.code.placeholder": "Authorization code",
|
|
166
166
|
"provider.connect.oauth.code.required": "Authorization code is required",
|
|
@@ -168,7 +168,7 @@ export const dict = {
|
|
|
168
168
|
"provider.connect.oauth.auto.visit.prefix": "Visit ",
|
|
169
169
|
"provider.connect.oauth.auto.visit.link": "this link",
|
|
170
170
|
"provider.connect.oauth.auto.visit.suffix":
|
|
171
|
-
" and enter the code below to connect your account and use {{provider}} models in
|
|
171
|
+
" and enter the code below to connect your account and use {{provider}} models in iKanban.",
|
|
172
172
|
"provider.connect.oauth.auto.confirmationCode": "Confirmation code",
|
|
173
173
|
"provider.connect.toast.connected.title": "{{provider}} connected",
|
|
174
174
|
"provider.connect.toast.connected.description":
|
|
@@ -486,7 +486,7 @@ export const dict = {
|
|
|
486
486
|
|
|
487
487
|
"toast.update.title": "Update available",
|
|
488
488
|
"toast.update.description":
|
|
489
|
-
"A new version of
|
|
489
|
+
"A new version of iKanban ({{version}}) is now available to install.",
|
|
490
490
|
"toast.update.action.installRestart": "Install and restart",
|
|
491
491
|
"toast.update.action.notYet": "Not yet",
|
|
492
492
|
|
|
@@ -497,7 +497,7 @@ export const dict = {
|
|
|
497
497
|
"error.page.action.checking": "Checking...",
|
|
498
498
|
"error.page.action.checkUpdates": "Check for updates",
|
|
499
499
|
"error.page.action.updateTo": "Update to {{version}}",
|
|
500
|
-
"error.page.report.prefix": "Please report this error to the
|
|
500
|
+
"error.page.report.prefix": "Please report this error to the iKanban team",
|
|
501
501
|
"error.page.report.discord": "on Discord",
|
|
502
502
|
"error.page.version": "Version: {{version}}",
|
|
503
503
|
|
|
@@ -550,7 +550,7 @@ export const dict = {
|
|
|
550
550
|
"notification.session.error.fallbackDescription": "An error occurred",
|
|
551
551
|
|
|
552
552
|
"home.recentProjects": "Recent projects",
|
|
553
|
-
"home.sessionBoard": "
|
|
553
|
+
"home.sessionBoard": "Ikanban",
|
|
554
554
|
"home.sessionBoard.description":
|
|
555
555
|
"Live session activity grouped into Progress and Idle.",
|
|
556
556
|
"home.sessionBoard.progress": "Progress",
|
|
@@ -667,7 +667,7 @@ export const dict = {
|
|
|
667
667
|
"sidebar.workspaces.disable": "Disable workspaces",
|
|
668
668
|
"sidebar.gettingStarted.title": "Getting started",
|
|
669
669
|
"sidebar.gettingStarted.line1":
|
|
670
|
-
"
|
|
670
|
+
"iKanban includes free models so you can start immediately.",
|
|
671
671
|
"sidebar.gettingStarted.line2":
|
|
672
672
|
"Connect any provider to use models, inc. Claude, GPT, Gemini etc.",
|
|
673
673
|
"sidebar.project.recentSessions": "Recent sessions",
|
|
@@ -695,12 +695,12 @@ export const dict = {
|
|
|
695
695
|
|
|
696
696
|
"settings.general.row.language.title": "Language",
|
|
697
697
|
"settings.general.row.language.description":
|
|
698
|
-
"Change the display language for
|
|
698
|
+
"Change the display language for iKanban",
|
|
699
699
|
"settings.general.row.appearance.title": "Appearance",
|
|
700
700
|
"settings.general.row.appearance.description":
|
|
701
|
-
"Customise how
|
|
701
|
+
"Customise how iKanban looks on your device",
|
|
702
702
|
"settings.general.row.theme.title": "Theme",
|
|
703
|
-
"settings.general.row.theme.description": "Customise how
|
|
703
|
+
"settings.general.row.theme.description": "Customise how iKanban is themed.",
|
|
704
704
|
"settings.general.row.font.title": "Font",
|
|
705
705
|
"settings.general.row.font.description":
|
|
706
706
|
"Customise the mono font used in code blocks",
|
|
@@ -731,7 +731,7 @@ export const dict = {
|
|
|
731
731
|
|
|
732
732
|
"settings.updates.row.startup.title": "Check for updates on startup",
|
|
733
733
|
"settings.updates.row.startup.description":
|
|
734
|
-
"Automatically check for updates when
|
|
734
|
+
"Automatically check for updates when iKanban launches",
|
|
735
735
|
"settings.updates.row.check.title": "Check for updates",
|
|
736
736
|
"settings.updates.row.check.description":
|
|
737
737
|
"Manually check for updates and install if available",
|
|
@@ -739,7 +739,7 @@ export const dict = {
|
|
|
739
739
|
"settings.updates.action.checking": "Checking...",
|
|
740
740
|
"settings.updates.toast.latest.title": "You're up to date",
|
|
741
741
|
"settings.updates.toast.latest.description":
|
|
742
|
-
"You're running the latest version of
|
|
742
|
+
"You're running the latest version of iKanban.",
|
|
743
743
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
|
744
744
|
"font.option.cascadiaCode": "Cascadia Code",
|
|
745
745
|
"font.option.firaCode": "Fira Code",
|
package/src/i18n/zh.ts
CHANGED
|
@@ -145,7 +145,7 @@ export const dict = {
|
|
|
145
145
|
"dialog.model.recent": "最近",
|
|
146
146
|
"dialog.model.manage.description": "自定义模型选择器中显示的模型。",
|
|
147
147
|
"dialog.model.manage.provider.toggle": "切换 {{provider}} 的所有模型",
|
|
148
|
-
"dialog.model.unpaid.freeModels.title": "
|
|
148
|
+
"dialog.model.unpaid.freeModels.title": "iKanban 提供的免费模型",
|
|
149
149
|
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
|
|
150
150
|
|
|
151
151
|
"dialog.provider.viewAll": "查看更多提供商",
|
|
@@ -158,7 +158,7 @@ export const dict = {
|
|
|
158
158
|
"provider.connect.status.waiting": "等待授权...",
|
|
159
159
|
"provider.connect.status.failed": "授权失败:{{error}}",
|
|
160
160
|
"provider.connect.apiKey.description":
|
|
161
|
-
"输入你的 {{provider}} API 密钥以连接帐户,并在
|
|
161
|
+
"输入你的 {{provider}} API 密钥以连接帐户,并在 iKanban 中使用 {{provider}} 模型。",
|
|
162
162
|
"provider.connect.apiKey.label": "{{provider}} API 密钥",
|
|
163
163
|
"provider.connect.apiKey.placeholder": "API 密钥",
|
|
164
164
|
"provider.connect.apiKey.required": "API 密钥为必填项",
|
|
@@ -172,7 +172,7 @@ export const dict = {
|
|
|
172
172
|
"provider.connect.oauth.code.visit.prefix": "访问 ",
|
|
173
173
|
"provider.connect.oauth.code.visit.link": "此链接",
|
|
174
174
|
"provider.connect.oauth.code.visit.suffix":
|
|
175
|
-
" 获取授权码,以连接你的帐户并在
|
|
175
|
+
" 获取授权码,以连接你的帐户并在 iKanban 中使用 {{provider}} 模型。",
|
|
176
176
|
"provider.connect.oauth.code.label": "{{method}} 授权码",
|
|
177
177
|
"provider.connect.oauth.code.placeholder": "授权码",
|
|
178
178
|
"provider.connect.oauth.code.required": "授权码为必填项",
|
|
@@ -180,7 +180,7 @@ export const dict = {
|
|
|
180
180
|
"provider.connect.oauth.auto.visit.prefix": "访问 ",
|
|
181
181
|
"provider.connect.oauth.auto.visit.link": "此链接",
|
|
182
182
|
"provider.connect.oauth.auto.visit.suffix":
|
|
183
|
-
" 并输入以下代码,以连接你的帐户并在
|
|
183
|
+
" 并输入以下代码,以连接你的帐户并在 iKanban 中使用 {{provider}} 模型。",
|
|
184
184
|
"provider.connect.oauth.auto.confirmationCode": "确认码",
|
|
185
185
|
"provider.connect.toast.connected.title": "{{provider}} 已连接",
|
|
186
186
|
"provider.connect.toast.connected.description":
|
|
@@ -461,7 +461,7 @@ export const dict = {
|
|
|
461
461
|
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
|
|
462
462
|
"toast.session.listFailed.title": "无法加载 {{project}} 的会话",
|
|
463
463
|
"toast.update.title": "有可用更新",
|
|
464
|
-
"toast.update.description": "
|
|
464
|
+
"toast.update.description": "iKanban 有新版本 ({{version}}) 可安装。",
|
|
465
465
|
"toast.update.action.installRestart": "安装并重启",
|
|
466
466
|
"toast.update.action.notYet": "稍后",
|
|
467
467
|
|
|
@@ -472,7 +472,7 @@ export const dict = {
|
|
|
472
472
|
"error.page.action.checking": "检查中...",
|
|
473
473
|
"error.page.action.checkUpdates": "检查更新",
|
|
474
474
|
"error.page.action.updateTo": "更新到 {{version}}",
|
|
475
|
-
"error.page.report.prefix": "请将此错误报告给
|
|
475
|
+
"error.page.report.prefix": "请将此错误报告给 iKanban 团队",
|
|
476
476
|
"error.page.report.discord": "在 Discord 上",
|
|
477
477
|
"error.page.version": "版本:{{version}}",
|
|
478
478
|
"error.dev.rootNotFound":
|
|
@@ -520,7 +520,7 @@ export const dict = {
|
|
|
520
520
|
"notification.session.error.fallbackDescription": "发生错误",
|
|
521
521
|
|
|
522
522
|
"home.recentProjects": "最近项目",
|
|
523
|
-
"home.sessionBoard": "
|
|
523
|
+
"home.sessionBoard": "Ikanban",
|
|
524
524
|
"home.sessionBoard.description": "按 Progress 和 Idle 分组。",
|
|
525
525
|
"home.sessionBoard.progress": "Progress",
|
|
526
526
|
"home.sessionBoard.idle": "Idle",
|
|
@@ -624,7 +624,7 @@ export const dict = {
|
|
|
624
624
|
"sidebar.workspaces.enable": "启用工作区",
|
|
625
625
|
"sidebar.workspaces.disable": "禁用工作区",
|
|
626
626
|
"sidebar.gettingStarted.title": "入门",
|
|
627
|
-
"sidebar.gettingStarted.line1": "
|
|
627
|
+
"sidebar.gettingStarted.line1": "iKanban 提供免费模型,你可以立即开始使用。",
|
|
628
628
|
"sidebar.gettingStarted.line2":
|
|
629
629
|
"连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。",
|
|
630
630
|
"sidebar.project.recentSessions": "最近会话",
|
|
@@ -652,12 +652,12 @@ export const dict = {
|
|
|
652
652
|
"settings.general.section.feed": "动态",
|
|
653
653
|
"settings.general.section.display": "显示",
|
|
654
654
|
"settings.general.row.language.title": "语言",
|
|
655
|
-
"settings.general.row.language.description": "更改
|
|
655
|
+
"settings.general.row.language.description": "更改 iKanban 的显示语言",
|
|
656
656
|
"settings.general.row.appearance.title": "外观",
|
|
657
657
|
"settings.general.row.appearance.description":
|
|
658
|
-
"自定义
|
|
658
|
+
"自定义 iKanban 在你的设备上的外观",
|
|
659
659
|
"settings.general.row.theme.title": "主题",
|
|
660
|
-
"settings.general.row.theme.description": "自定义
|
|
660
|
+
"settings.general.row.theme.description": "自定义 iKanban 的主题。",
|
|
661
661
|
"settings.general.row.font.title": "字体",
|
|
662
662
|
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
|
663
663
|
"settings.general.row.reasoningSummaries.title": "显示推理摘要",
|
|
@@ -682,14 +682,14 @@ export const dict = {
|
|
|
682
682
|
"settings.general.action.releaseNotesCurrent": "查看变更",
|
|
683
683
|
|
|
684
684
|
"settings.updates.row.startup.title": "启动时检查更新",
|
|
685
|
-
"settings.updates.row.startup.description": "在
|
|
685
|
+
"settings.updates.row.startup.description": "在 iKanban 启动时自动检查更新",
|
|
686
686
|
"settings.updates.row.check.title": "检查更新",
|
|
687
687
|
"settings.updates.row.check.description": "手动检查更新并在有更新时安装",
|
|
688
688
|
"settings.updates.action.checkNow": "立即检查",
|
|
689
689
|
"settings.updates.action.checking": "正在检查...",
|
|
690
690
|
"settings.updates.toast.latest.title": "已是最新版本",
|
|
691
691
|
"settings.updates.toast.latest.description":
|
|
692
|
-
"你正在使用最新版本的
|
|
692
|
+
"你正在使用最新版本的 iKanban。",
|
|
693
693
|
|
|
694
694
|
"font.option.ibmPlexMono": "IBM Plex Mono",
|
|
695
695
|
"font.option.cascadiaCode": "Cascadia Code",
|
|
@@ -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>
|
|
@@ -101,7 +101,7 @@ const SessionRow = (props: {
|
|
|
101
101
|
cancelHoverPrefetch: () => void
|
|
102
102
|
}): JSX.Element => (
|
|
103
103
|
<A
|
|
104
|
-
href={`/${props.slug}
|
|
104
|
+
href={`/${props.slug}/${props.session.id}`}
|
|
105
105
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
|
|
106
106
|
onPointerEnter={props.scheduleHoverPrefetch}
|
|
107
107
|
onPointerLeave={props.cancelHoverPrefetch}
|
|
@@ -310,7 +310,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
|
|
|
310
310
|
onMessageSelect={(message) => {
|
|
311
311
|
if (!isActive()) {
|
|
312
312
|
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
|
|
313
|
-
navigate(
|
|
313
|
+
navigate(`/${props.slug}/${props.session.id}`)
|
|
314
314
|
return
|
|
315
315
|
}
|
|
316
316
|
window.history.replaceState(null, "", `#message-${message.id}`)
|
|
@@ -360,7 +360,7 @@ export const NewSessionItem = (props: {
|
|
|
360
360
|
const tooltip = () => props.mobile || !props.sidebarExpanded()
|
|
361
361
|
const item = (
|
|
362
362
|
<A
|
|
363
|
-
href={`/${props.slug}
|
|
363
|
+
href={`/${props.slug}`}
|
|
364
364
|
end
|
|
365
365
|
class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
|
|
366
366
|
onClick={() => {
|
|
@@ -8,7 +8,7 @@ import { Icon } from "ikanban-ui/icon"
|
|
|
8
8
|
import { IconButton } from "ikanban-ui/icon-button"
|
|
9
9
|
import { Tooltip } from "ikanban-ui/tooltip"
|
|
10
10
|
import { createSortable } from "@thisbeyond/solid-dnd"
|
|
11
|
-
import {
|
|
11
|
+
import { type LocalProject } from "@/context/layout"
|
|
12
12
|
import { useGlobalSync } from "@/context/global-sync"
|
|
13
13
|
import { useLanguage } from "@/context/language"
|
|
14
14
|
import { useNotification } from "@/context/notification"
|
|
@@ -77,7 +77,6 @@ const ProjectTile = (props: {
|
|
|
77
77
|
language: ReturnType<typeof useLanguage>
|
|
78
78
|
}): JSX.Element => {
|
|
79
79
|
const notification = useNotification()
|
|
80
|
-
const layout = useLayout()
|
|
81
80
|
const unseenCount = createMemo(() =>
|
|
82
81
|
props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
|
|
83
82
|
)
|
|
@@ -127,7 +126,6 @@ const ProjectTile = (props: {
|
|
|
127
126
|
onClick={() => {
|
|
128
127
|
if (props.selected()) {
|
|
129
128
|
props.setSuppressHover(true)
|
|
130
|
-
layout.sidebar.toggle()
|
|
131
129
|
return
|
|
132
130
|
}
|
|
133
131
|
props.setSuppressHover(false)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const sidebarExpanded = (mobile: boolean | undefined,
|
|
1
|
+
export const sidebarExpanded = (mobile: boolean | undefined, _opened: boolean) => !!mobile
|
|
@@ -6,8 +6,8 @@ describe("sidebarExpanded", () => {
|
|
|
6
6
|
expect(sidebarExpanded(true, false)).toBe(true)
|
|
7
7
|
})
|
|
8
8
|
|
|
9
|
-
test("
|
|
10
|
-
expect(sidebarExpanded(false, true)).toBe(
|
|
9
|
+
test("stays closed on desktop now that the sidebar pane is removed", () => {
|
|
10
|
+
expect(sidebarExpanded(false, true)).toBe(false)
|
|
11
11
|
expect(sidebarExpanded(false, false)).toBe(false)
|
|
12
12
|
})
|
|
13
13
|
})
|
|
@@ -437,7 +437,7 @@ export const SortableWorkspace = (props: {
|
|
|
437
437
|
root={props.project.worktree}
|
|
438
438
|
setHoverSession={props.ctx.setHoverSession}
|
|
439
439
|
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
|
|
440
|
-
navigateToNewSession={() => navigate(`/${slug()}
|
|
440
|
+
navigateToNewSession={() => navigate(`/${slug()}`)}
|
|
441
441
|
/>
|
|
442
442
|
</div>
|
|
443
443
|
</div>
|