opencode-session-todo 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +31 -2
- package/USAGE.md +83 -0
- package/dist/tui.js +433 -0
- package/docs/development.md +28 -0
- package/index.ts +211 -0
- package/package.json +63 -4
- package/rpc.ts +22 -0
- package/src/diff.ts +64 -0
- package/src/instructions.ts +10 -0
- package/src/model.ts +51 -0
- package/src/sidebar.ts +44 -0
- package/src/store.ts +162 -0
- package/src/todotxt.ts +150 -0
- package/tui.tsx +299 -0
package/src/todotxt.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
import { Todo, TodoError, Todos } from "./model.ts"
|
|
3
|
+
|
|
4
|
+
export type Record =
|
|
5
|
+
& Pick<Todo, "content" | "priority">
|
|
6
|
+
& { readonly created: string | undefined }
|
|
7
|
+
& (
|
|
8
|
+
| { readonly status: "pending" | "in_progress" }
|
|
9
|
+
| {
|
|
10
|
+
readonly status: "completed" | "cancelled"
|
|
11
|
+
readonly completed: string | undefined
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const Letters = Schema.Literals(["A", "B", "C"])
|
|
16
|
+
type Letter = typeof Letters.Type
|
|
17
|
+
const priorities: Readonly<{ A: "high"; B: "medium"; C: "low" }> = {
|
|
18
|
+
A: "high",
|
|
19
|
+
B: "medium",
|
|
20
|
+
C: "low",
|
|
21
|
+
}
|
|
22
|
+
const letters: Readonly<{ high: "A"; medium: "B"; low: "C" }> = {
|
|
23
|
+
high: "A",
|
|
24
|
+
medium: "B",
|
|
25
|
+
low: "C",
|
|
26
|
+
}
|
|
27
|
+
const STATUS_PREFIX: string = "status:"
|
|
28
|
+
const PRIORITY_PREFIX: string = "pri:"
|
|
29
|
+
|
|
30
|
+
export interface Document {
|
|
31
|
+
readonly records: readonly Record[]
|
|
32
|
+
readonly todos: Todos
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const decodeLetter = (value: unknown): Effect.Effect<Letter, TodoError> =>
|
|
36
|
+
Schema.decodeUnknownEffect(Letters)(value).pipe(
|
|
37
|
+
Effect.mapError(
|
|
38
|
+
(): TodoError => new TodoError({ message: "Supported todo.txt priorities are A, B, and C" }),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const parseLine = Effect.fnUntraced(function*(line: string): Effect.fn.Return<Record, TodoError> {
|
|
43
|
+
const closed: boolean = line.startsWith("x ")
|
|
44
|
+
let body: string = closed ? line.slice(2) : line
|
|
45
|
+
let letter: Letter | undefined
|
|
46
|
+
const priority: RegExpMatchArray | null = body.match(/^\(([A-Z])\) /)
|
|
47
|
+
if (priority) {
|
|
48
|
+
letter = yield* decodeLetter(priority[1])
|
|
49
|
+
body = body.slice(priority[0].length)
|
|
50
|
+
}
|
|
51
|
+
const datePrefix: RegExpMatchArray | null = body.match(
|
|
52
|
+
closed ? /^\d{4}-\d{2}-\d{2} (?:\d{4}-\d{2}-\d{2} )?/ : /^\d{4}-\d{2}-\d{2} /,
|
|
53
|
+
)
|
|
54
|
+
const dates: string = datePrefix?.[0] ?? ""
|
|
55
|
+
body = body.slice(dates.length)
|
|
56
|
+
let statusToken: string | undefined
|
|
57
|
+
const words: string[] = []
|
|
58
|
+
for (const word of body.split(" ")) {
|
|
59
|
+
if (word.startsWith(STATUS_PREFIX)) {
|
|
60
|
+
if (statusToken !== undefined) {
|
|
61
|
+
return yield* new TodoError({ message: "Duplicate status: token" })
|
|
62
|
+
}
|
|
63
|
+
statusToken = word.slice(STATUS_PREFIX.length)
|
|
64
|
+
} else if (word.startsWith(PRIORITY_PREFIX)) {
|
|
65
|
+
if (letter !== undefined) return yield* new TodoError({ message: "Duplicate priority" })
|
|
66
|
+
letter = yield* decodeLetter(word.slice(PRIORITY_PREFIX.length))
|
|
67
|
+
} else {
|
|
68
|
+
words.push(word)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const status: string = statusToken ?? (closed ? "completed" : "pending")
|
|
72
|
+
if (closed !== (status === "completed" || status === "cancelled")) {
|
|
73
|
+
return yield* new TodoError({ message: "The x marker and status: token disagree" })
|
|
74
|
+
}
|
|
75
|
+
const todo: Todo = yield* Schema.decodeUnknownEffect(Todo)({
|
|
76
|
+
content: words.join(" ").trim(),
|
|
77
|
+
status,
|
|
78
|
+
priority: priorities[letter ?? "B"],
|
|
79
|
+
}).pipe(Effect.mapError((error): TodoError => new TodoError({ message: error.message })))
|
|
80
|
+
const [first, second] = dates.trim().split(" ")
|
|
81
|
+
switch (todo.status) {
|
|
82
|
+
case "pending":
|
|
83
|
+
case "in_progress":
|
|
84
|
+
return { ...todo, status: todo.status, created: dates ? first : undefined }
|
|
85
|
+
case "completed":
|
|
86
|
+
case "cancelled":
|
|
87
|
+
return { ...todo, status: todo.status, completed: dates ? first : undefined, created: second }
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export const parse = Effect.fnUntraced(function*(
|
|
92
|
+
text: string,
|
|
93
|
+
): Effect.fn.Return<Document, TodoError> {
|
|
94
|
+
const records: Record[] = []
|
|
95
|
+
let lineNumber: number = 0
|
|
96
|
+
for (const line of text.replace(/^\uFEFF/, "").split(/\r?\n/)) {
|
|
97
|
+
lineNumber += 1
|
|
98
|
+
if (!line.trim()) continue
|
|
99
|
+
const record: Record = yield* parseLine(line).pipe(
|
|
100
|
+
Effect.mapError(
|
|
101
|
+
(error): TodoError =>
|
|
102
|
+
new TodoError({ message: `todo.txt line ${lineNumber}: ${error.message}` }),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
records.push(record)
|
|
106
|
+
}
|
|
107
|
+
const todos: Todos = yield* Schema.decodeUnknownEffect(Todos)(records).pipe(
|
|
108
|
+
Effect.mapError((error): TodoError => new TodoError({ message: error.message })),
|
|
109
|
+
)
|
|
110
|
+
return { records, todos }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const renderLine = (todo: Todo, old: Record | undefined, today: string): string => {
|
|
114
|
+
const created: string | undefined = old?.created
|
|
115
|
+
const openBody: string = created ? `${created} ${todo.content}` : todo.content
|
|
116
|
+
const letter: Letter = letters[todo.priority]
|
|
117
|
+
switch (todo.status) {
|
|
118
|
+
case "pending":
|
|
119
|
+
return `(${letter}) ${openBody}\n`
|
|
120
|
+
case "in_progress":
|
|
121
|
+
return `(${letter}) ${openBody} status:in_progress\n`
|
|
122
|
+
case "completed":
|
|
123
|
+
case "cancelled": {
|
|
124
|
+
const completed: string | undefined =
|
|
125
|
+
old?.status === "completed" || old?.status === "cancelled"
|
|
126
|
+
? old.completed
|
|
127
|
+
: created
|
|
128
|
+
? today
|
|
129
|
+
: undefined
|
|
130
|
+
const closedBody: string = completed ? `${completed} ${openBody}` : todo.content
|
|
131
|
+
const status: string = todo.status === "cancelled" ? " status:cancelled" : ""
|
|
132
|
+
return `x ${closedBody} pri:${letter}${status}\n`
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const render = (todos: Todos, previous: readonly Record[], today: string): string => {
|
|
138
|
+
const records: Map<Todo["content"], Record[]> = previous.reduceRight(
|
|
139
|
+
(grouped: Map<Todo["content"], Record[]>, record: Record): Map<Todo["content"], Record[]> => {
|
|
140
|
+
const existing: Record[] | undefined = grouped.get(record.content)
|
|
141
|
+
if (existing) existing.push(record)
|
|
142
|
+
else grouped.set(record.content, [record])
|
|
143
|
+
return grouped
|
|
144
|
+
},
|
|
145
|
+
new Map<Todo["content"], Record[]>(),
|
|
146
|
+
)
|
|
147
|
+
return todos.map(
|
|
148
|
+
(todo: Todo): string => renderLine(todo, records.get(todo.content)?.pop(), today),
|
|
149
|
+
).join("")
|
|
150
|
+
}
|
package/tui.tsx
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
2
|
+
import type { KeymapLayer, PanelInput } from "@opencode/plugin/tui/context"
|
|
3
|
+
import { Session } from "@opencode/schema/session"
|
|
4
|
+
import type { ResolvedTheme } from "@opencode/theme/tui"
|
|
5
|
+
import type { MouseEvent } from "@opentui/core"
|
|
6
|
+
import type { JSX } from "@opentui/solid"
|
|
7
|
+
import { Effect, Result, Schema } from "effect"
|
|
8
|
+
import {
|
|
9
|
+
type Accessor,
|
|
10
|
+
createEffect,
|
|
11
|
+
createMemo,
|
|
12
|
+
createSignal,
|
|
13
|
+
For,
|
|
14
|
+
onCleanup,
|
|
15
|
+
Show,
|
|
16
|
+
} from "solid-js"
|
|
17
|
+
import { TodoRpc } from "./rpc.ts"
|
|
18
|
+
import { type Snapshot, type Todo } from "./src/model.ts"
|
|
19
|
+
import { projectTodos, SidebarOptions, type TodoProjection } from "./src/sidebar.ts"
|
|
20
|
+
|
|
21
|
+
type View =
|
|
22
|
+
| { readonly status: "loading" }
|
|
23
|
+
| { readonly status: "ready"; readonly snapshot: Snapshot }
|
|
24
|
+
| { readonly status: "error"; readonly message: string }
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
readonly sessionID: string
|
|
28
|
+
readonly full?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const symbols: Readonly<Record<Todo["status"], string>> = {
|
|
32
|
+
pending: "[ ]",
|
|
33
|
+
in_progress: "[•]",
|
|
34
|
+
completed: "[✓]",
|
|
35
|
+
cancelled: "[–]",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const colorFor = (
|
|
39
|
+
status: Todo["status"],
|
|
40
|
+
theme: ResolvedTheme,
|
|
41
|
+
): ResolvedTheme["text"]["base"] => {
|
|
42
|
+
switch (status) {
|
|
43
|
+
case "pending":
|
|
44
|
+
return theme.text.base
|
|
45
|
+
case "in_progress":
|
|
46
|
+
return theme.text.feedback.warning.base
|
|
47
|
+
case "completed":
|
|
48
|
+
return theme.text.muted
|
|
49
|
+
case "cancelled":
|
|
50
|
+
return theme.text.feedback.error.base
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const Message = Schema.Struct({ message: Schema.String })
|
|
55
|
+
const messageFor = (error: unknown): string =>
|
|
56
|
+
Schema.is(Message)(error) ? error.message : String(error)
|
|
57
|
+
|
|
58
|
+
export default Plugin.define({
|
|
59
|
+
id: "session-todo",
|
|
60
|
+
setup(ctx): () => void {
|
|
61
|
+
const options = Schema.decodeUnknownResult(SidebarOptions)(ctx.options)
|
|
62
|
+
if (Result.isFailure(options)) {
|
|
63
|
+
ctx.ui.toast.show({
|
|
64
|
+
title: "Invalid TODO sidebar options",
|
|
65
|
+
message: options.failure.message,
|
|
66
|
+
variant: "error",
|
|
67
|
+
})
|
|
68
|
+
return (): void => {}
|
|
69
|
+
}
|
|
70
|
+
const api = ctx.client.rpc(TodoRpc)
|
|
71
|
+
const [sidebarView, updateSidebarView] = ctx.storage.store("sidebar", {
|
|
72
|
+
initial: { open: true },
|
|
73
|
+
})
|
|
74
|
+
const notify: () => void = api.events.on("updated", (event): void => {
|
|
75
|
+
if (event.data.diff === "") return
|
|
76
|
+
ctx.ui.toast.show({
|
|
77
|
+
title: "TODO updated",
|
|
78
|
+
message: event.data.diff,
|
|
79
|
+
variant: "info",
|
|
80
|
+
sessionID: event.data.sessionID,
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
const List = (props: Props): JSX.Element => {
|
|
84
|
+
const [view, setView] = createSignal<View>({ status: "loading" })
|
|
85
|
+
createEffect((): void => {
|
|
86
|
+
const sessionID: string = props.sessionID
|
|
87
|
+
setView({ status: "loading" })
|
|
88
|
+
const controller: AbortController = new AbortController()
|
|
89
|
+
let running: boolean = false
|
|
90
|
+
const refresh = (): void => {
|
|
91
|
+
if (running || controller.signal.aborted) return
|
|
92
|
+
running = true
|
|
93
|
+
const request: Effect.Effect<Snapshot, unknown> = Schema.decodeUnknownEffect(Session.ID)(
|
|
94
|
+
sessionID,
|
|
95
|
+
).pipe(
|
|
96
|
+
Effect.flatMap(
|
|
97
|
+
(id): Effect.Effect<Snapshot, unknown> =>
|
|
98
|
+
Effect.tryPromise({
|
|
99
|
+
try: (): Promise<Snapshot> =>
|
|
100
|
+
api.read(
|
|
101
|
+
{ sessionID: id },
|
|
102
|
+
{
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
location: ctx.data.session.get(sessionID)?.location
|
|
105
|
+
?? ctx.location
|
|
106
|
+
?? ctx.data.location.default(),
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
catch: (error): unknown => error,
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
void Effect.runPromise(
|
|
114
|
+
request.pipe(
|
|
115
|
+
Effect.match({
|
|
116
|
+
onSuccess: (snapshot): View => ({ status: "ready", snapshot }),
|
|
117
|
+
onFailure: (error): View => ({ status: "error", message: messageFor(error) }),
|
|
118
|
+
}),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
.then((next): void => {
|
|
122
|
+
if (!controller.signal.aborted) setView(next)
|
|
123
|
+
})
|
|
124
|
+
.catch((error: unknown): void => {
|
|
125
|
+
if (!controller.signal.aborted) {
|
|
126
|
+
setView({ status: "error", message: messageFor(error) })
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
.finally((): void => {
|
|
130
|
+
running = false
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
refresh()
|
|
134
|
+
const timer: ReturnType<typeof setInterval> = setInterval(refresh, 2000)
|
|
135
|
+
const unsubscribe: () => void = api.events.on("updated", (event): void => {
|
|
136
|
+
if (event.data.sessionID === sessionID) refresh()
|
|
137
|
+
})
|
|
138
|
+
onCleanup((): void => {
|
|
139
|
+
controller.abort()
|
|
140
|
+
clearInterval(timer)
|
|
141
|
+
unsubscribe()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const snapshot: Accessor<Snapshot | undefined> = (): Snapshot | undefined => {
|
|
146
|
+
const current: View = view()
|
|
147
|
+
return current.status === "ready" ? current.snapshot : undefined
|
|
148
|
+
}
|
|
149
|
+
const projection: Accessor<TodoProjection | undefined> = createMemo(
|
|
150
|
+
(): TodoProjection | undefined => {
|
|
151
|
+
const current: Snapshot | undefined = snapshot()
|
|
152
|
+
return current === undefined
|
|
153
|
+
? undefined
|
|
154
|
+
: projectTodos(current.todos, props.full ? "full" : options.success.sidebarLimit)
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
const todos: Accessor<readonly Todo[]> = (): readonly Todo[] => projection()?.items ?? []
|
|
158
|
+
const active: Accessor<number> = (): number => projection()?.remaining ?? 0
|
|
159
|
+
const more: Accessor<number | undefined> = (): number | undefined => {
|
|
160
|
+
const current: TodoProjection | undefined = projection()
|
|
161
|
+
return current?.kind === "limited" ? current.total : undefined
|
|
162
|
+
}
|
|
163
|
+
const error: Accessor<string | undefined> = (): string | undefined => {
|
|
164
|
+
const current: View = view()
|
|
165
|
+
return current.status === "error" ? current.message : undefined
|
|
166
|
+
}
|
|
167
|
+
const visible: Accessor<boolean> = (): boolean =>
|
|
168
|
+
Boolean(props.full || snapshot()?.todos.length || error())
|
|
169
|
+
return (
|
|
170
|
+
<Show when={visible()}>
|
|
171
|
+
<box flexDirection="column" gap={0} paddingTop={1}>
|
|
172
|
+
<box
|
|
173
|
+
flexDirection="row"
|
|
174
|
+
gap={1}
|
|
175
|
+
onMouseDown={(event: MouseEvent): void => {
|
|
176
|
+
if (props.full) return
|
|
177
|
+
event.stopPropagation()
|
|
178
|
+
void updateSidebarView((draft): void => {
|
|
179
|
+
draft.open = !draft.open
|
|
180
|
+
}).catch((error: unknown): void => {
|
|
181
|
+
ctx.ui.toast.show({
|
|
182
|
+
title: "Todo sidebar",
|
|
183
|
+
message: `Cannot save expanded state: ${messageFor(error)}`,
|
|
184
|
+
variant: "error",
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<Show when={!props.full}>
|
|
190
|
+
<text fg={ctx.theme.text.base}>{sidebarView.open ? "▼" : "▶"}</text>
|
|
191
|
+
</Show>
|
|
192
|
+
<text fg={ctx.theme.text.base}>
|
|
193
|
+
<b>TODO</b>
|
|
194
|
+
<span style={{ fg: ctx.theme.text.muted }}>{" "}({active()} remaining)</span>
|
|
195
|
+
</text>
|
|
196
|
+
</box>
|
|
197
|
+
<Show when={props.full || sidebarView.open}>
|
|
198
|
+
<Show when={props.full}>
|
|
199
|
+
<text fg={ctx.theme.text.muted}>{snapshot()?.path ?? "Loading todo.txt…"}</text>
|
|
200
|
+
</Show>
|
|
201
|
+
<Show when={error()}>
|
|
202
|
+
<text fg={ctx.theme.text.feedback.error.base}>Cannot read todos: {error()}</text>
|
|
203
|
+
</Show>
|
|
204
|
+
<Show when={props.full && view().status === "ready" && todos().length === 0}>
|
|
205
|
+
<text fg={ctx.theme.text.muted}>No todos for this session.</text>
|
|
206
|
+
</Show>
|
|
207
|
+
<For each={todos()}>
|
|
208
|
+
{(todo): JSX.Element => (
|
|
209
|
+
<text fg={colorFor(todo.status, ctx.theme)}>
|
|
210
|
+
{symbols[todo.status]} {todo.content} · {todo.priority}
|
|
211
|
+
</text>
|
|
212
|
+
)}
|
|
213
|
+
</For>
|
|
214
|
+
<Show when={more()}>
|
|
215
|
+
<text
|
|
216
|
+
fg={ctx.theme.text.feedback.info.base}
|
|
217
|
+
onMouseDown={(event: MouseEvent): void => {
|
|
218
|
+
event.stopPropagation()
|
|
219
|
+
ctx.ui.panel.open("session-todo.list")
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
Show all {more()} todos →
|
|
223
|
+
</text>
|
|
224
|
+
</Show>
|
|
225
|
+
</Show>
|
|
226
|
+
</box>
|
|
227
|
+
</Show>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sidebar: () => void = ctx.ui.slot({
|
|
232
|
+
append: "sidebar.content",
|
|
233
|
+
render: (props): JSX.Element => <List sessionID={props.sessionID} />,
|
|
234
|
+
})
|
|
235
|
+
const Panel = (props: PanelInput): JSX.Element => {
|
|
236
|
+
ctx.keymap.layer(
|
|
237
|
+
(): KeymapLayer => ({
|
|
238
|
+
mode: "global",
|
|
239
|
+
enabled: props.focused,
|
|
240
|
+
commands: [
|
|
241
|
+
{
|
|
242
|
+
bind: "escape",
|
|
243
|
+
title: "Close session todos",
|
|
244
|
+
run: props.close,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
return (
|
|
250
|
+
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
251
|
+
<text
|
|
252
|
+
fg={ctx.theme.text.base}
|
|
253
|
+
onMouseDown={(event: MouseEvent): void => {
|
|
254
|
+
event.stopPropagation()
|
|
255
|
+
props.close()
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
Close × · Esc when focused · Ctrl+P to toggle
|
|
259
|
+
</text>
|
|
260
|
+
<scrollbox flexGrow={1}>
|
|
261
|
+
<List sessionID={props.sessionID} full />
|
|
262
|
+
</scrollbox>
|
|
263
|
+
</box>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
const panel: () => void = ctx.ui.slot({
|
|
267
|
+
append: "session.panel",
|
|
268
|
+
render: (props): JSX.Element => (
|
|
269
|
+
<Show when={props.name === "session-todo.list"}>
|
|
270
|
+
<Panel {...props} />
|
|
271
|
+
</Show>
|
|
272
|
+
),
|
|
273
|
+
})
|
|
274
|
+
ctx.keymap.layer(
|
|
275
|
+
(): KeymapLayer => ({
|
|
276
|
+
mode: "global",
|
|
277
|
+
commands: [
|
|
278
|
+
{
|
|
279
|
+
id: "session-todo.list",
|
|
280
|
+
title: "Toggle session todos",
|
|
281
|
+
group: "Session",
|
|
282
|
+
palette: true,
|
|
283
|
+
slash: { name: "todos" },
|
|
284
|
+
enabled: ctx.ui.router.current().type === "session",
|
|
285
|
+
run: (): void => {
|
|
286
|
+
if (ctx.ui.panel.current()?.name === "session-todo.list") ctx.ui.panel.close()
|
|
287
|
+
else ctx.ui.panel.open("session-todo.list")
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
return (): void => {
|
|
294
|
+
sidebar()
|
|
295
|
+
panel()
|
|
296
|
+
notify()
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
})
|