@suchitraswain/nightcode-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/nightcode.cjs +10 -0
- package/bin/nightcode.ts +5 -0
- package/package.json +50 -0
- package/src/bootstrap-env.ts +33 -0
- package/src/components/border.tsx +18 -0
- package/src/components/command-menu/commands.tsx +147 -0
- package/src/components/command-menu/filter-commands.ts +8 -0
- package/src/components/command-menu/index.tsx +74 -0
- package/src/components/command-menu/types.ts +20 -0
- package/src/components/command-menu/use-command-menu.ts +113 -0
- package/src/components/dialog-search-list.tsx +127 -0
- package/src/components/dialogs/agents-dialog.tsx +47 -0
- package/src/components/dialogs/index.tsx +4 -0
- package/src/components/dialogs/models-dialog.tsx +41 -0
- package/src/components/dialogs/sessions-dialog.tsx +94 -0
- package/src/components/dialogs/theme-dialog.tsx +58 -0
- package/src/components/header.tsx +10 -0
- package/src/components/input-bar.tsx +611 -0
- package/src/components/messages/bot-message.tsx +160 -0
- package/src/components/messages/error-message.tsx +36 -0
- package/src/components/messages/index.tsx +3 -0
- package/src/components/messages/user-message.tsx +36 -0
- package/src/components/session-shell.tsx +65 -0
- package/src/components/spinner.tsx +14 -0
- package/src/components/status-bar.tsx +23 -0
- package/src/hooks/use-chat.ts +107 -0
- package/src/hosted-config.ts +6 -0
- package/src/index.tsx +29 -0
- package/src/layouts/root-layout.tsx +25 -0
- package/src/layouts/themed-root.tsx +21 -0
- package/src/lib/api-client.ts +25 -0
- package/src/lib/auth.ts +38 -0
- package/src/lib/http-errors.ts +18 -0
- package/src/lib/local-tools.ts +170 -0
- package/src/lib/oauth.ts +166 -0
- package/src/lib/upgrade.ts +27 -0
- package/src/providers/dialog/index.tsx +123 -0
- package/src/providers/dialog/types.ts +6 -0
- package/src/providers/keyboard-layer/index.tsx +98 -0
- package/src/providers/prompt-config/index.tsx +52 -0
- package/src/providers/theme/index.tsx +75 -0
- package/src/providers/toast/index.tsx +118 -0
- package/src/providers/toast/types.ts +9 -0
- package/src/screens/home.tsx +39 -0
- package/src/screens/new-session.tsx +82 -0
- package/src/screens/session.tsx +171 -0
- package/src/theme.ts +568 -0
- package/vendor/shared/api-types.ts +11 -0
- package/vendor/shared/index.ts +20 -0
- package/vendor/shared/models.ts +72 -0
- package/vendor/shared/schemas.ts +87 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useDialog } from "../../providers/dialog";
|
|
3
|
+
import { DialogSearchList } from "../dialog-search-list";
|
|
4
|
+
import { Mode } from "@nightcode/database/enums";
|
|
5
|
+
import type { SupportedChatModelId } from "@nightcode/shared";
|
|
6
|
+
|
|
7
|
+
type ModelsDialogContentProps = {
|
|
8
|
+
models: SupportedChatModelId[];
|
|
9
|
+
onSelectModel: (modelId: SupportedChatModelId) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ModelsDialogContent = ({
|
|
13
|
+
models,
|
|
14
|
+
onSelectModel
|
|
15
|
+
}: ModelsDialogContentProps) => {
|
|
16
|
+
const dialog = useDialog();
|
|
17
|
+
|
|
18
|
+
const handleSelect = useCallback(
|
|
19
|
+
(modelId: SupportedChatModelId) => {
|
|
20
|
+
onSelectModel(modelId);
|
|
21
|
+
dialog.close();
|
|
22
|
+
},
|
|
23
|
+
[dialog, onSelectModel],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<DialogSearchList
|
|
28
|
+
items={models}
|
|
29
|
+
onSelect={handleSelect}
|
|
30
|
+
filterFn={(modelId, query) => modelId.toLowerCase().includes(query.toLowerCase())}
|
|
31
|
+
renderItem={(modelId, isSelected) => (
|
|
32
|
+
<text selectable={false} fg={isSelected ? "black" : "white"}>
|
|
33
|
+
{modelId}
|
|
34
|
+
</text>
|
|
35
|
+
)}
|
|
36
|
+
getKey={(modelId) => modelId}
|
|
37
|
+
placeholder="Search models"
|
|
38
|
+
emptyText="No matching models"
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { format } from "date-fns";
|
|
4
|
+
import { useNavigate } from "react-router";
|
|
5
|
+
import { useDialog } from "../../providers/dialog";
|
|
6
|
+
import { useToast } from "../../providers/toast";
|
|
7
|
+
import { apiClient } from "../../lib/api-client";
|
|
8
|
+
import { getErrorMessage } from "../../lib/http-errors";
|
|
9
|
+
import { DialogSearchList } from "../dialog-search-list";
|
|
10
|
+
import type { SessionSummary } from "@nightcode/shared";
|
|
11
|
+
|
|
12
|
+
export const SessionsDialogContent = () => {
|
|
13
|
+
const [sessions, setSessions] = useState<SessionSummary[]>([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const { close } = useDialog();
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const { show } = useToast();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let ignore = false;
|
|
21
|
+
|
|
22
|
+
const fetchSessions = async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await apiClient.sessions.$get();
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(await getErrorMessage(res));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
|
|
31
|
+
if (!ignore) {
|
|
32
|
+
setSessions(data);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!ignore) {
|
|
37
|
+
show({
|
|
38
|
+
variant: "error",
|
|
39
|
+
message: error instanceof Error ? error.message : "Failed to fetch sessions",
|
|
40
|
+
});
|
|
41
|
+
close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
fetchSessions();
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
ignore = true;
|
|
50
|
+
};
|
|
51
|
+
}, [close, show]);
|
|
52
|
+
|
|
53
|
+
const handleSelect = useCallback(
|
|
54
|
+
(session: Session) => {
|
|
55
|
+
close();
|
|
56
|
+
navigate(`/sessions/${session.id}`);
|
|
57
|
+
},
|
|
58
|
+
[close, navigate],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (loading) {
|
|
62
|
+
return (
|
|
63
|
+
<box flexDirection="column">
|
|
64
|
+
<text attributes={TextAttributes.DIM}>Loading sessions...</text>
|
|
65
|
+
</box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<DialogSearchList
|
|
71
|
+
items={sessions}
|
|
72
|
+
onSelect={handleSelect}
|
|
73
|
+
filterFn={(s, query) => s.title.toLowerCase().includes(query.toLowerCase())}
|
|
74
|
+
renderItem={(session, isSelected) => (
|
|
75
|
+
<>
|
|
76
|
+
<text selectable={false} fg={isSelected ? "black" : "white"}>
|
|
77
|
+
{session.title}
|
|
78
|
+
</text>
|
|
79
|
+
<box flexGrow={1} />
|
|
80
|
+
<text
|
|
81
|
+
selectable={false}
|
|
82
|
+
fg={isSelected ? "black" : undefined}
|
|
83
|
+
attributes={TextAttributes.DIM}
|
|
84
|
+
>
|
|
85
|
+
{format(new Date(session.createdAt), "hh:mm a")}
|
|
86
|
+
</text>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
getKey={(s) => s.id}
|
|
90
|
+
placeholder="Search sessions"
|
|
91
|
+
emptyText="No matching sessions"
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { useDialog } from "../../providers/dialog";
|
|
3
|
+
import { useTheme } from "../../providers/theme";
|
|
4
|
+
import { DialogSearchList } from "../dialog-search-list";
|
|
5
|
+
import { THEMES } from "../../theme";
|
|
6
|
+
import type { Theme } from "../../theme";
|
|
7
|
+
|
|
8
|
+
export const ThemeDialogContent = () => {
|
|
9
|
+
const dialog = useDialog();
|
|
10
|
+
const { setTheme, currentTheme } = useTheme();
|
|
11
|
+
const originalThemeRef = useRef(currentTheme);
|
|
12
|
+
const confirmedRef = useRef(false);
|
|
13
|
+
|
|
14
|
+
// Revert to original theme if the user dismisses without confirming
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
return () => {
|
|
17
|
+
if (!confirmedRef.current) {
|
|
18
|
+
setTheme(originalThemeRef.current);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}, [setTheme]);
|
|
22
|
+
|
|
23
|
+
const handleSelect = useCallback(
|
|
24
|
+
(theme: Theme) => {
|
|
25
|
+
confirmedRef.current = true;
|
|
26
|
+
setTheme(theme);
|
|
27
|
+
dialog.close();
|
|
28
|
+
},
|
|
29
|
+
[setTheme, dialog],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const handleHighlight = useCallback(
|
|
33
|
+
(theme: Theme) => {
|
|
34
|
+
setTheme(theme);
|
|
35
|
+
},
|
|
36
|
+
[setTheme],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<DialogSearchList
|
|
41
|
+
items={THEMES}
|
|
42
|
+
onSelect={handleSelect}
|
|
43
|
+
onHighlight={handleHighlight}
|
|
44
|
+
filterFn={(t, query) => t.name.toLowerCase().includes(query.toLowerCase())}
|
|
45
|
+
renderItem={(theme, isSelected) => (
|
|
46
|
+
<text selectable={false} fg={isSelected ? "black" : "white"}>
|
|
47
|
+
{theme.name === originalThemeRef.current.name
|
|
48
|
+
? "\u0020\u2022\u0020"
|
|
49
|
+
: "\u0020\u0020\u0020"}
|
|
50
|
+
{theme.name}
|
|
51
|
+
</text>
|
|
52
|
+
)}
|
|
53
|
+
getKey={(t) => t.name}
|
|
54
|
+
placeholder="Search themes"
|
|
55
|
+
emptyText="No matching themes"
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function Header() {
|
|
2
|
+
return (
|
|
3
|
+
<box justifyContent="center" alignItems="center">
|
|
4
|
+
<box flexDirection="row" justifyContent="center" gap={0.5} alignItems="center">
|
|
5
|
+
<ascii-font font="tiny" text="Night" color="gray" />
|
|
6
|
+
<ascii-font font="tiny" text="Code" />
|
|
7
|
+
</box>
|
|
8
|
+
</box>
|
|
9
|
+
);
|
|
10
|
+
};
|